左求值表示式,堆疊,除錯陷阱與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欄位堆疊的情況是:
- 呼叫方法 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(委託函式方式):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] 這個條件。
這個錯誤出現的情況並不常見,簡單說就是只有完全且同時符合以下的情況,才會產生問題:
- 當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 以上支援,如果程式中有動態構造查詢條件的情況,請大家及時獲取最新的原始碼。