1. 程式人生 > 其它 >左求值表示式,堆疊,除錯陷阱與ORM查詢語言的設計

左求值表示式,堆疊,除錯陷阱與ORM查詢語言的設計

1,表示式的求值順序與堆疊結構

“表示式” 是程式語言一個很重要的術語,也是大家天天寫的程式中很常見的東西,但是表示式的求值順序一定是從左到右麼? C/C++語言中沒有明確規定表示式的運算順序(從左到右,或是從右到左),這點與C#及Java語言都不同。不過可以確定的是,C#表示式的求值順序一定是從左到右的。這個問題雖然對於大多數情況來說不重要,甚至很多普通C#,Java開發者都會忽略的問題,但是對於語言設計者,框架設計者,這是有可能需要考慮的問題。

堆疊是2種資料結構,“棧” 是一種後進先出的資料結構,也就是說後存放的先取,先存放的後取。這就如同我們要取出放在箱子裡面底下的東西,我們首先要移開壓在它上面的物體。這個特點常用於函式的巢狀呼叫,用於記錄每一次函式呼叫的點,以便下級函式呼叫完畢後返回該記錄點繼續執行,最典型的應用就是函式的遞迴呼叫。

根據表示式的求值順序,再結合堆疊結構,程式語言就可以知道表示式的呼叫結構,知道方法引數的求值順序,SOD框架恰好利用了這個特徵來構建ORM查詢語言--OQL

2,“欄位堆疊”與實體類屬性呼叫的“祕密”

OQL內建了一個堆疊物件:

 /// <summary>
/// 欄位堆疊
/// </summary>
protected internal Stack<TableNameField> fieldStack = new Stack<TableNameField>();

這個堆疊記憶體放的是表名稱欄位物件,它的定義是:

 public class TableNameField
    {
        /// <summary>
        /// 獲取表名稱
        /// </summary>
        public string Name {  get;}
        /// <summary>
        /// 原始欄位名
        /// </summary>
        public string Field;
        /// <summary>
        /// 關聯的實體類
        /// </summary>
        public EntityBase Entity;
        /// <summary>
        /// 在一系列欄位使用中的索引號
        /// </summary>
        public int Index;
        /// <summary>
        /// 欄位對應的值
        /// </summary>
        public object FieldValue;
      
        /// <summary>
        /// 在SQL語句中使用的欄位名
        /// </summary>
        public string SqlFieldName
        {
           get;set;
        }
    }

在每一個OQL物件上,都有關聯的SOD框架的實體類,它有一個“屬性訪問事件”,OQL物件訂閱了該事件:

public class OQL
{
        /// <summary>
        /// 欄位堆疊
        /// </summary>
        protected internal Stack<TableNameField> fieldStack = new Stack<TableNameField>();

        public OQL(EntityBase e)
        {
            //其它略
            e.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(e_PropertyGetting);
        }

        void e_PropertyGetting(object sender, PropertyGettingEventArgs e)
        {
            TableNameField tnf = new TableNameField()
            {
                Field = e.PropertyName,
                Entity = (EntityBase)sender,
                Index = this.GetFieldGettingIndex()
            };
         
            fieldStack.Push(tnf);
        }

        //其它方法略
}

這樣,在OQL例項表示式中,每一次呼叫關聯的實體類的屬性,就會將該屬性對應的欄位名資訊,壓入欄位堆疊。這些欄位資訊,將用來構造SQL的 Select,Where,Order 子句,本篇將講解它是如何構造Where條件子句的。

OQL的Where方法支援多種條件構造方式,其中一種是使用OQLCompare物件來做條件。由於OQLCompare 物件設計成了OQL的子物件,因此它也能訪問 fieldStack 物件,利用它提供的資訊,構造條件資訊。

    /// <summary>
    /// 實體物件條件比較類,用於複雜條件比較表示式
    /// </summary>
    public class OQLCompare //: IDisposable
    {
       /// <summary>
        /// 關聯的OQL物件
        /// </summary>
        public OQL LinkedOQL { get;protected internal set; }

        public OQLCompare(OQL oql)
        {
            if (oql == null)
                throw new ArgumentException("OQLCompare 關聯的OQL物件為空!");
            this.LinkedOQL = oql;
        }

       //其它內容略
   }

此後,就可以像下面這樣構造並使用一個OQL查詢物件:

User user=new User();
OQL q=OQL.From(user)
  .Select(user.ID,user.Name)
  .Where(cmp=>cmp.Comparer(user.Age,">",18))
.END;
List<User> users=EntityQuery<User>.QueryList(q);

這個OQL查詢是在查詢所有年齡大於18歲的使用者,在Where方法中,cmp物件就是一個OQLCompare 物件,它的Comparer方法使用了user物件的Age屬性,在方法執行的時候,user.Age 被求值,欄位名“Age” 被壓入OQL的欄位堆疊,

Stack:(0--“Age”)

於是,OQL可以構造出類似下面的SQL語句:

Select ID ,Name From Tb_User
  Where Age > @P0
-- P0 = 18

當然我們可以直接呼叫OQL的方法,打印出SQL語句和引數資訊,下面會說。

聰明的讀者你可能想到了,這是在利用表示式求值得“副作用”啊,本來只是對 user.Age 屬性求值而已,但卻利用該屬性求值過程中引發的事件,得到了使用的欄位資訊,然後利用這個資訊來構造SQL語句!

這是一個“巧妙”的運用,OQL避開了反射,也沒有使用"表示式樹",所以OQL生成SQL的過程非常高效,不會有EF的第一次查詢非常慢的問題。

在OQLCompare物件的Comparer方法上,第三個引數除了是一個要比較的值,也可以是另外一個欄位,例如下面的查詢規則定義的符合最低年齡設定的使用者:

User user=new User();
Rule rule = new Rule();
OQL q=OQL.From(user)
  .InnerJoin(rule).On(user.RuleID,rule.ID)
  .Select(user.ID,user.Name)
  .Where(cmp=>cmp.Comparer(user.Age,">",rule.LowAge))
.END;
List<User> users=EntityQuery<User>.QueryList(q);

該查詢會生成下面的SQL語句:

Select M.ID,M.Name
  From Tb_User M
    Inner Join Tb_Rule T0 ON M.RuleID = T0.ID 
    Where M.Age > T0.LowAge

 在這個查詢中,OQLCompare物件使用的OQL欄位堆疊的情況是:

  1. 呼叫方法 Comparer
  2. 求取 uer.Age屬性,得到 "M.Age" 欄位名,壓入欄位堆疊;
  3. 求取 rule.LowAg屬性, 得到 "T0.LowAge" 欄位名,壓入欄位堆疊;

假設此時程式執行在除錯狀態,在這裡有一個斷點中斷了,在VS的IDE 上查看了其它屬性的值,比如看了下 user.ID,user.Name,那麼此時OQL的堆疊資料是:

Stack:(0--“M.ID”,1--“M.Name”)

當方法Comparer 執行後,堆疊的結果是:

Stack:(0--“T0.LowAge”,1--“M.Age”, 2--“M.ID”,3--“M.Name”)

呼叫OQL方法,生成條件字串的時候,從該堆疊彈出欄位資訊:

Pop Stack:0--“T0.LowAge”

Pop Stack:1--“M.Age”

 實際上,在OQLComare物件的Comparer方法中進行了上面的堆疊“彈出”操作,並且返回了一個新的 OQLCompare 物件,根據C#語言的“左求值表示式”原則 ,這個新的OQLCompare 物件獲得了下面的資訊:

compare.ComparedFieldName ="M.Age" ;
compare.ComparedParameterName ="T0.LowAge" ;
compare.ComparedType =">" ;

該資訊完全表達了構建OQL查詢的“原意“,並指導生成正確的查詢條件:

M.Age > T0.LowAge

由於每次呼叫Comparer方法都生成了這樣的一個新的 OQLCompare 物件,所以整個OQLCompare 物件是一個“組合物件”,組合中有根,有枝條,有葉子,組合成為一個“條件物件樹”,有這樣一棵樹,那麼再複雜的查詢條件,都可以表示了。

3,動態構造查詢條件與“除錯陷阱”

從上面的舉例,我們發現OQLCompare物件即能夠進行【欄位與值】進行比較,又能夠進行【欄位與欄位】的條件比較,而且也能識別不同表的欄位在一起進行比較。 

但是,在這個過程中,有可能遭遇”除錯陷阱“。

3.1,欄位堆疊--避免“除錯陷阱”

回看開始的例子:

User user=new User();
OQL q=OQL.From(user)
  .Select(user.ID,user.Name)
  .Where(cmp=>cmp.Comparer(user.Age,">",18))
.END;
List<User> users=EntityQuery<User>.QueryList(q);

加入有色背景處是一個斷點,程式執行到這裡進入除錯模式,而此時滑鼠放在了 user.ID上面,那麼當方法執行到 Comparer裡面去以後,我們來看看堆疊的結果:

Stack:(0--“Age”,1--“ID”)

在方法執行過程中,首先彈出第一個值:

Pop Stack:0--“Age”

但是SOD框架並不知道這個欄位資訊是 Comparer方法的第一個引數,還是第三個引數,不過拿 user.Age 的值跟第三個引數的值 18 進行比較,user.Age !=18 ,所以可以斷定,欄位資訊”Age“ 發生在方法的第一個引數呼叫上,而不是第三個引數,因此,欄位堆疊的第二個元素,(1-- ”ID“) 也就沒有必要彈出了,等到方法執行完成,將Stack 欄位堆疊清除即可,這樣在下一次呼叫開始的時候,不會造成干擾。

所以這裡的情況是在除錯的時候,給欄位堆疊增加了新的元素,如果此時 user.Age==18 ,那麼 cmp.Comparer(user.Age,">",18) 不會生成預期的SQL,從而產生”除錯陷阱“。產生這個問題的具體原因,請看下面的內容。

當然,當前小節這個OQL查詢在非除錯狀態下執行是沒有問題的,欄位堆疊的執行原理可以避免”除錯陷阱“的問題。

3.2,動態構造查詢條件的 類“除錯陷阱”

上面的欄位堆疊處理方案並不能完全化解”除錯陷阱“的問題,而且,有時候這個問題不是發生在除錯狀態,也有可能發生在動態構造條件的過程中,請參考下面的例子:

        void TestIfCondition2()
        {
            Users user = new Users() { ID = 0, NickName = "abc", UserName="zhang san", Password="pwd111" };
            OQL q7 = OQL.From(user)
                .Select()
                .Where<Users>(CreateCondition)
                .END;
            Console.WriteLine("OQL by 動態構建 OQLCompare Test(委託函式方式):rn{0}", q7);
            Console.WriteLine(q7.PrintParameterInfo());
        }

        OQLCompare CreateCondition(OQLCompare cmp, Users user)
        {
            OQLCompare cmpResult = null;
            if (user.NickName != "")
                cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
            // 上面一行,也可以採用這樣的寫法: cmpResult = cmp.EqualValue(user.NickName);
            if (user.ID > 0)
                cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
            else
                cmpResult = cmpResult & cmp.Comparer(user.UserName, "=", "zhang san")  
                                      & cmp.Comparer(user.Password, "=", "pwd111");
            return cmpResult;
        }

執行這個程式,會輸出下面的SQL語句和引數資訊:

OQL by 動態構建 OQLCompare Test(委託函式方式):
SELECT  [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],
[LastLoginTime],[LastLoginIP],[Remarks],[AddTime]
FROM [LT_Users]
     WHERE    [NickName] = @P0 AND  [ID] =  [UserName]  AND  [Password] = @P1
--------OQL Parameters information----------
 have 2 parameter,detail:
  @P0=abc        Type:String
  @P1=pwd111     Type:String
------------------End------------------------

請注意SQL條件中的背景標註部分,[ID] =  [UserName] 這個條件,顯然不是我們期望的,出現這個問題的原因是什麼呢? 原來問題出在這個程式段:

if (user.ID > 0)
                cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
            else
                cmpResult = cmpResult & cmp.Comparer(user.UserName, "=", "zhang san")  
                                      & cmp.Comparer(user.Password, "=", "pwd111");

程式選擇了 else 分支,執行了cmp.Comparer(user.UserName, "=", "zhang san")  這句,但是,在本例中,user.UserName 的值恰好就是 “zhang san”,所以 Comparer方法的第一個引數和第三個引數的值是一樣的,而此時的OQL堆疊的資料是:

Stack:(0--“UserName”,1--“ID”)

OQL會首先彈出堆疊的元素 "UserName" 欄位,然後讓它對應的實體類屬性值與Comparer方法的第三個引數值進行比較,發現這2個值是相同的,於是假設"UserName"欄位呼叫發生在Comparer方法的第三個引數上,於是繼續彈出OQL欄位堆疊的下一個元素:

Pop Stack:1--“ID”

於是將欄位名“ID” 作為Comparer方法的第一個引數呼叫的“副作用”結果,構造成了 [ID] =  [UserName] 這個條件。

這個錯誤出現的情況並不常見,簡單說就是只有完全且同時符合以下的情況,才會產生問題:

  1. 當Comparer方法執行前,呼叫過OQL關聯的實體類的屬性(既屬性求值),(如果最近的一次實體類屬性呼叫發生在OQLCompare物件的某個方法內則不符合本條件)
  2. 且方法的第一個引數和第三個引數的值一樣的時候,
  3. 第三個引數不是一個實體類屬性呼叫,而是一個單純變數或者值

3.3,消除複雜查詢條件的“欄位堆疊“干擾

要解決這個問題也很容易,將上面的程式碼改寫成下面這個樣子:

 OQLCompare CreateCondition(OQLCompare cmp, Users user)
 {
     OQLCompare cmpResult = null;
     if (user.NickName != "")
         cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
     // 上面一行,也可以採用這樣的寫法: cmpResult = cmp.EqualValue(user.NickName);
     if (user.ID > 0)
         cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
     else
         cmpResult = cmpResult & cmp.EqualValue(user.UserName)  
                               & cmp.Comparer(user.Password, "=", "pwd111");
     return cmpResult;
 }

這裡將使用 user.UserName 自身的值進行相等比較,避免了欄位堆疊的影響。如果不是自身的值相等比較,那麼還可以利用操作符過載,進行更多的比較方式,比如大於,小於等:

 OQLCompare CreateCondition(OQLCompare cmp, Users user)
 {
     OQLCompare cmpResult = null;
     if (user.NickName != "")
         cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
     // 上面一行,也可以採用這樣的寫法: cmpResult = cmp.EqualValue(user.NickName);
     if (user.ID > 0)
         cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
     else
         cmpResult = cmpResult & cmp.Property(user.UserName) == "zhang san" 
                               & cmp.Comparer(user.Password, "=", "pwd111");
     return cmpResult;
 }

如果出於效能上的考慮或者進行Like 查詢等,必須使用Comparer 方法,要解決這種“屬性與比較的值相等”的OQL堆疊欄位干擾問題,還可呼叫OQLCompare物件的的NewCompare方法:

 OQLCompare CreateCondition(OQLCompare cmp, Users user)
 {
     OQLCompare cmpResult = null;
     if (user.NickName != "")
         cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
     // 上面一行,也可以採用這樣的寫法: cmpResult = cmp.EqualValue(user.NickName);
     if (user.ID > 0)
         cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
     else
         cmpResult = cmpResult & cmp.NewCompare().Comparer(user.UserName,"=", "zhang san") 
                               & cmp.Comparer(user.Password, "=", "pwd111");
     return cmpResult;
 }

如果覺得上面的方式繁瑣,那麼還有一個更直接的辦法,就是動態構造條件的時候,不在關聯的實體類上呼叫屬性進行條件判斷,而是建立另外一個實體類物件(不可以使用克隆的方式):

 OQLCompare CreateCondition(OQLCompare cmp, Users user)
 {
     Users testUser = new Users {  NickName =user.NickName , ID =user.ID};

     OQLCompare cmpResult = null;
     if (testUser.NickName != "")
         cmpResult = cmp.Comparer(user.NickName, "=", user.NickName);
     // 上面一行,也可以採用這樣的寫法: cmpResult = cmp.EqualValue(user.NickName);
     if (testUser.ID > 0)
         cmpResult = cmpResult & cmp.Comparer(user.ID, "=", user.ID);
     else
         cmpResult = cmpResult & cmp.Comparer(user.UserName,"=", "zhang san") 
                               & cmp.Comparer(user.Password, "=", "pwd111");
     return cmpResult;
 }

當然,可能最簡單的方式,還是你有意讓Comparer 方法的第一實體類屬性值引數和第三個普通值引數的值不要相等,這在大多數情況下都是可以做到的。

採用上面的方式處理後,對於OQL動態構造查詢條件,可以得到下面正確的SQL資訊:

OQL by 動態構建 OQLCompare Test(委託函式方式):
SELECT  [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],
[LastLoginTime],[LastLoginIP],[Remarks],[AddTime]
FROM [LT_Users]
     WHERE    [NickName] = @P0 AND  [UserName] = @P1  AND  [Password] = @P2
--------OQL Parameters information----------
 have 3 parameter,detail:
  @P0=abc        Type:String
  @P1=zhang san          Type:String
  @P2=pwd111     Type:String
------------------End------------------------

 小節

本篇說明了程式語言左求值表示式規則,堆疊資料結構,並利用這兩個特徵,結合屬性呼叫事件 ,巧妙的設計了SOD框架的”ORM查詢語言“--OQL,並詳細的分析了可能產生的問題與解決方案。如果使用PDF.NET SOD框架來處理動態的查詢條件,那麼本篇文章一定要仔細閱讀一下。

感謝大家一直以來對於PDF.NET SOD框架的支援,

框架官網地址:http://www.pwmis.com/sqlmap

開源專案地址:http://pwms.codeplex.com

注意:本文的解決方案和例項程式,需要SOD框架的新版本 5.2.3.0429 以上支援,如果程式中有動態構造查詢條件的情況,請大家及時獲取最新的原始碼。