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字段堆棧的情況是:
- 調用方法 Comparer
- 求取 uer.Age屬性,得到 "M.Age" 字段名,壓入字段堆棧;
- 求取 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(委托函數方式):\r\n{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] 這個條件。
這個錯誤出現的情況並不常見,簡單說就是只有完全且同時符合以下的情況,才會產生問題:
- 當Comparer方法執行前,調用過OQL關聯的實體類的屬性(既屬性求值),(如果最近的一次實體類屬性調用發生在OQLCompare對象的某個方法內則不符合本條件)
- 且方法的第一個參數和第三個參數的值一樣的時候,
- 第三個參數不是一個實體類屬性調用,而是一個單純變量或者值
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 以上支持,如果程序中有動態構造查詢條件的情況,請大家及時獲取最新的源代碼。
