相關文章內容索引:
PDF.NET框架的OQL經過“脫胎換骨”般的重構之后,引來了它華麗麗的新篇章,將“對象化的SQL”特征發揮到極致,與至於我在Q群里面說這應該算是OQL的“收山之作”了。然而,我這么說有什么依據?它的設計哲學是什么?它究竟是何樣?由於本文篇幅較長,請聽本篇慢慢道來,敘說它的廬山真面目!
[有圖有真相]
User user=new User();

注意:圖上的表達式中的形參 parent 其實是OQL對象,這里表示父級OQL對象,它參與構造它的子OQL對象。
(圖4:高級子查詢)

(圖5:SQL鎖)
三、精簡之道
PDF.NET誕生的背景就是在2006年,我參與一個使用iBatis.Net的項目,當時沒有找到合適的代碼生成工具,手工寫各種配置,寫SQL映射寫的吐血,心想這么復雜的東西為何還得到那么多人的喜歡?也許功能的確很強大,而復雜正是體現了它的強大,而我,天生就是一個只喜歡“簡單”的程序猿,因此選擇了.NET而不是Java平台,既然如此何必要用iBatis這種復雜的東西?
於是,我參考iBatis的特點,將它大力精簡,僅保留了SQL簡單的映射功能,將SQL語句寫到一個配置文件里面,然后提供一套生成工具來自動生成DAL代碼,這便是PDF.NET的SQL-MAP功能。盡管從使用上已經比iBatis.Net簡單很多了,但是對於大多數簡單的CRUD,還是需要寫SQL映射,實在不爽,於是給框架加入了點ORM功能,但覺得當時的ORM動不動就將實體類的全部屬性字段的數據返回來,也覺得不爽,於是設計了OQL,來操作ORM。這便是PDF.NET Ver 1.0 誕生的故事。
盡管已經過去7年多時間,PDF.NET不斷發展,但主線還是它的特色功能SQL-MAP、OQL、數據控件這三大部分。最近一段時間,我對OQL進行了完全的重構,仍然堅守最初的設計理念,做最簡單最易用的數據框架。下面,就從OQL的查詢API設計,來講下這個理念。
3.1,Select血案—委托之殤
ORM選取實體字段的Select方法該怎樣設計?象EF這樣子:
var user = from c in db.User select new { c.UserID, c.Accounts, c.Point, c.SavePoint, c.LoginServerID };
EF使用Linq作為ORM查詢語言,Linq是語法糖,它本質上會轉化成下面的Lambda方式:
Var user=db.User.Select(c=>new { c.UserID, c.Accounts, c.Point, c.SavePoint, c.LoginServerID });
而Lambda,常常用來簡化匿名委托的寫法,也就是說,Lambda也是委托的一種語法糖。因此,EF實現這個效果,靠的還是委托這個功能。
上面是單個實體類的實體屬性字段選取,如果是多個呢?
我們知道,Linq可以處理多個實體類的連接(左、右、內)查詢,但百度了半天,實在不知道怎么用Lambda來實現多個實體類的屬性選取,有知道的還請大俠告之,謝謝。
假設有一個集合C,它內部包含了2個集合A,B連接處理后的數據,我們這樣來使用Select方法:
Var data=C.Select((a,b)=>new { F1=a.F1, F2=b.F2 });
大家注意到Select方法需要傳遞2個參數進去,此時對參數的類型推導可能會成為問題,因此,實際上的Select擴展方法的定義應該帶有2個類型的泛型方法的,調用的其實是下面的方法:
Var data=C.Select<A,B>((a,b)=>new { F1=a.F1, F2=b.F2 });
假如有3個類型的參數呢?自然得像下面這個樣子使用:
Var data=C.Select<X,Y,Z>((x,y,z)=>new { F1=x.F1, F2=y.F2, F3=z.F3 });
假如還有4個、5個。。。。N個類型的參數呢?豈不是要定義含有N個參數的Select擴展方法?
如果還有其它方法,假設Where方法也有這種問題呢?
My God!
委托方法盡管保證了我們寫的代碼是強類型的,一旦遇到方法需要的類型過多那么麻煩也就越多,還是回過頭來說ORM查詢的select問題,假設使用委托的解決方案怎么看都不是一個最佳的方案,特別是多實體類查詢的時候。
PDF.NET的ORM查詢語言OQL很早就注意到了這個問題,所以它的Select方法采用了非泛型化的設計,例如單個實體類屬性字段選取:
OQL q = OQL.From(user) .Select(user.ID, user.UserName, user.RoleID) .END;
多個實體類的屬性字段選取:
OQL q2 = OQL.From(user) .InnerJoin(roles).On(user.RoleID, roles.ID) .Select(user.RoleID, roles.RoleName) .END;
從上面的例子看出來了,不管OQL查詢幾個實體,它的Select使用方式始終是一致的,要想使用哪個屬性字段,在Select方法里面通過“實體類實例.屬性”的方式,一直寫下去即可,而支持這個功能僅使用了C#的可選參數功能:
public OQL1 Select(params object[] fields) { //具體實現略 }
方法沒有使用委托參數,也沒有定義N多泛型重載,就輕易地實現了我們的目標,從這個意義上來說,在這里使用委托,真是委托之殤啊!
3.2,Where迷途—委托神器
3.2.1,最簡單的Where條件比較方法
OQL的Where 條件構造支持最簡單的使用方式,如果查詢條件都是“相等比較”方式,那么可以使用下面的方式:
Users user = new Users() { NickName = "pdf.net", RoleID=5 }; OQL q0 = OQL.From(user) .Select() .Where(user.NickName,user.RoleID) .OrderBy(user.ID) .END; q0.SelectStar = true; Console.WriteLine("q0:one table and select all fields \r\n{0}", q0); Console.WriteLine(q0.PrintParameterInfo());
程序輸出的結果是:
q0:one table and select all fields SELECT * FROM [LT_Users] WHERE [NickName]=@P0 AND [RoleID]=@P1 ORDER BY [ID] --------OQL Parameters information---------- have 2 parameter,detail: @P0=pdf.net Type:String @P1=5 Type:Int32 ------------------End------------------------
可見Where 這樣使用非常簡單,實際項目中很多也是想等條件查詢的,采用這種方式不僅僅構造了查詢參數,而且將參數值也順利的設置好了,這就是使用ORM的實體類實例調用 方式最大的好處。
Where方法支持多個這樣的實體類參數,該方法在PDF.NET Ver4.X之前就一直支持。下面是它的定義:
public OQL2 Where(params object[] fields) { //其它代碼略 }
3.2.1,升級到V5版OQLCompare的問題
OQL的Where方法支持使用OQLCompare對象。在文章前面的2.6 OQLCompare –比較對象的組合模式 一節中說道我們通過它我們處理了長達5000行業務代碼構造的查詢條件,在PDF.NET Ver 4.X 版本中,OQLCompare對象得這樣使用:
User user=New User(); OQLCompare cmp=new OQLCompare(user); OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) & cmp.Compare(user.Password,”=”,”123456”); If(xxx條件) { cmpResult=cmpResult & …… //其它條件 } //條件對象構造完成 OQL q=OQL.From(user).Select().Where(cmpResult).END;
如果前面的代碼不修改,那么使用新版PDF.NET編譯時候不會出錯,但運行時會出錯:
OQLCompare 關聯的OQL對象為空!
3.2.2,OQLCompare新的構造函數
PDF.NET Ver 5.0版本之后,OQLCompare不通過實體類來初始化此對象,而是用對應的OQL對象來構造它,所以前面的代碼需要改造成下面這個樣子:
User user=New User(); OQL q=new OQL(user); OQLCompare cmp=new OQLCompare(q); OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) & cmp.Compare(user.Password,”=”,”123456”); If(xxx條件) { cmpResult=cmpResult & …… //其它條件 } //條件對象構造完成 q.Select().Where(cmpResult);
3.2.3,OQLCompare 條件比較委托
除了上面的修改方式,我們也可以不調整代碼的順序,僅作小小的改變:
User user=new User(); //OQLCompare cmp=new OQLCompare(user); OQLCompareFun cmpFun = cmp=> { OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”)
& cmp.Compare(user.Password,”=”,”123456”); if(xxx條件) { cmpResult=cmpResult & cmp.Compare(....)…… //AND其它條件 }
return cmpResult; } //條件對象構造完成 //OQL q=OQL.From(user).Select().Where(cmpResult).END; OQL q=OQL.From(user).Select().Where(cmpFun ).END;
這里,我們的Where方法接受了一個OQLCompareFun 委托參數,我們來看這個新版的Where方法是怎么實現的:
public OQL2 Where(OQLCompareFunc cmpFun) { OQLCompare compare = new OQLCompare(this.CurrentOQL); OQLCompare cmpResult = cmpFun(compare); return GetOQL2ByOQLCompare(cmpResult); }
原來沒啥神器的代碼,僅僅在方法內部聲明了一個新的OQLCompare對象,它使用了傳入OQL對象的構造函數,然后將這個OQLCompare對象實例給OQLCompare委托方法使用。但是,我們不能小看這個小小的改進,它將具體OQLCompare對象的處理延遲到了頂層OQL對象的實例化之后,這個“延遲執行”的特點,大大簡化了我們原來的代碼。
3.2.4,委托進階--泛型委托
前面的代碼還是稍微復雜了點,我們來看一個簡單的例子:
User user=new User(); Var list=OQL.From (user) .Select() .Where(cmp=> cmp.Property( user.RoleId)==10) .END .ToList<User>();
我們僅使用了2行代碼查詢到了用戶表中所有用戶的角色ID等於10的記錄,我們在一行代碼之內完成了條件代碼的編寫。
對於前面的代碼,我們還能不能繼續簡化呢?如果我們的Where用的委托參數能夠接受一個實體類參數,那么User對象的實例不必在這里聲明了。
下面是OQLCompare的委托方法定義:
public delegate OQLCompare OQLCompareFunc<T>(OQLCompare cmp, T p);
然后再定義一個對應的Where方法:
public OQL2 Where<T>(OQLCompareFunc<T> cmpFun) where T : EntityBase { OQLCompare compare = new OQLCompare(this.CurrentOQL); T p1 = GetInstance<T>(); OQLCompare cmpResult = cmpFun(compare, p1); return GetOQL2ByOQLCompare(cmpResult); }
OK,有了這個委托神器,前面的代碼終於可以一行搞定了:
Var list=OQL.From<User>() .Select() .Where<User>((cmp,user)=> cmp.Property( user.RoleId)==10) .END .ToList<User>();
不錯,委托真是厲害!
按照上面的思路如法炮制,我們定義最多有3個泛型類型的OQLCompare泛型委托,下面是全部的委托定義:
public delegate OQLCompare OQLCompareFunc(OQLCompare cmp); public delegate OQLCompare OQLCompareFunc<T>(OQLCompare cmp, T p); public delegate OQLCompare OQLCompareFunc<T1,T2>(OQLCompare cmp,T1 p1,T2 p2); public delegate OQLCompare OQLCompareFunc<T1, T2,T3>(OQLCompare cmp, T1 p1, T2 p2,T3 p3);
有了它,我們再來看一個復雜點的例子:
void Test4() { OQLCompareFunc<Users, UserRoles> cmpResult = (cmp, U, R) => ( cmp.Property(U.UserName) == "ABC" & cmp.Comparer(U.Password, "=", "111") & cmp.Comparer(R.RoleName, "=", "Role1") ) | ( (cmp.Comparer(U.UserName, "=", "CDE") & cmp.Property(U.Password) == "222" & cmp.Comparer(R.RoleName, "like", "%Role2") ) | (cmp.Property(U.LastLoginTime) > DateTime.Now.AddDays(-1)) ) ; Users user = new Users(); UserRoles roles = new UserRoles() { RoleName = "role1" }; OQL q4 = OQL.From(user) .InnerJoin(roles) .On(user.RoleID, roles.ID) .Select() .Where(cmpResult) .END; Console.WriteLine("OQL by OQLCompareFunc<T1,T2> Test:\r\n{0}", q4); Console.WriteLine(q4.PrintParameterInfo()); q4.Dispose(); }
下面是對應的SQL語句和參數信息:
OQL by OQLCompareFunc<T1,T2> Test: SELECT M.*,T0.* FROM [LT_Users] M INNER JOIN [LT_UserRoles] T0 ON M.[RoleID] = T0.[ID] WHERE ( M.[UserName] = @P0 AND M.[Password] = @P1 AND T0.[RoleName] = @P2 ) OR ( ( M.[UserName] = @P3 AND M.[Password] = @P4 AND T0.[RoleName] LIKE @P5 ) OR M.[LastLoginTime] > @P6 ) --------OQL Parameters information---------- have 7 parameter,detail: @P0=ABC Type:String @P1=111 Type:String @P2=Role1 Type:String @P3=CDE Type:String @P4=222 Type:String @P5=%Role2 Type:String @P6=2013/7/28 17:31:35 Type:DateTime ------------------End------------------------
3.2.5,委托與閉包
前面我們說道只定義到了3個泛型參數的OQLCompareFun委托,為啥不再繼續定義更多參數的泛型委托?
我覺得,這個問題從3方面考慮:
- A,如果你需要連接3個以上的表進行查詢,那么你的查詢設計過於復雜,可以從數據庫或者系統設計上去避免;
- B,泛型具有閉包功能,可以將需要的參數傳遞進去;
- C,如果定義更多的OQLCompare泛型委托,有可能重蹈“委托之殤”。
如果你不贊成A的說法,查詢一定得有3個以上的情況,那么你可以應用B的方式。實際上,該方式前面已經舉例過了,再來看一個實際的例子:
void Test3() { Users user = new Users(); UserRoles roles = new UserRoles() { RoleName = "role1" }; OQLCompareFunc cmpResult = cmp => ( cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111") & cmp.EqualValue(roles.RoleName) ) | ( (cmp.Comparer(user.UserName, OQLCompare.CompareType.Equal, "BCD") & cmp.Property(user.Password) == 222 & cmp.Comparer(roles.ID, "in", new int[] { 1,2,3 }) ) | (cmp.Property(user.LastLoginTime) > DateTime.Now.AddDays(-1)) ) ; OQL q3 = OQL.From(user).InnerJoin(roles) .On(user.RoleID, roles.ID) .Select() .Where(cmpResult) .END; Console.WriteLine("OQL by OQLCompareFunc Test:\r\n{0}", q3); Console.WriteLine(q3.PrintParameterInfo()); }
我們在cmpResult 委托的結果中,使用了委托變量之外的參數對象user和roles 。如果有更多的參數委托方法也是可以使用的,這些參數就是委托中的“閉包”,使用該特性,那么再復雜的問題都能夠處理了。
再次感嘆,委托,真乃神器也!
3.3,Having 之旅—重用之歡
我們再重溫一下1.2.4的Having問題,看看那個SQL語句:
SELECT SalesOrderID, SUM(LineTotal) AS SubTotal FROM Sales.SalesOrderDetail GROUP BY SalesOrderID HAVING SUM(LineTotal) > 100000.00 ORDER BY SalesOrderID ;
Having是對分組Group之后的再次篩選,而Where是在Group之前的,所以本質上Having子句也是一個條件表達式,但由於相對Where要簡單,我們先用個方法來實現:
public OQL4 Having(object field,object Value,string sqlFunctionFormat) { //具體代碼略 }
使用的時候這樣用即可:
OQL q5 = OQL.From(user) .Select(user.RoleID).Count(user.RoleID, "count_rolid") .GroupBy(user.RoleID) .Having(user.RoleID, 2,”COUNT{0} > {1} ”)) .END;
上面這種使用方式,首先需要手寫Having的聚合函數條件,不是很方便,OQL的Where方法可以使用OQLCompare對象作為比較條件,那么Having也是可以使用的,將Having方法改寫下:
public OQL4 Having(OQLCompareFunc cmpFun) { OQLCompare compare = new OQLCompare(this.CurrentOQL); OQLCompare cmpResult = cmpFun(compare); if (!object.Equals(cmpResult, null)) { CurrentOQL.oqlString += "\r\n HAVING " + cmpResult.CompareString; } return new OQL4(CurrentOQL); }
然后OQL中就可以下面這樣使用:
OQL q5 = OQL.From(user) .Select(user.RoleID).Count(user.RoleID, "count_rolid") .GroupBy(user.RoleID) .Having(p => p.Count(user.RoleID, OQLCompare.CompareType.GreaterThanOrEqual, 2)) .END; Console.WriteLine("q5:having Test: \r\n{0}", q5); Console.WriteLine(q5.PrintParameterInfo());
程序輸出:
q5:having Test: SELECT [RoleID] ,COUNT( [RoleID]) AS count_rolid FROM [LT_Users] GROUP BY [RoleID] HAVING COUNT( [RoleID]) >= @P0 --------OQL Parameters information---------- have 1 parameter,detail: @P0=2 Type:Int32 ------------------End------------------------
OQLCompare 成功應用於Having方法,找到問題的類似之處,然后重用問題的解決方案,這不是令人非常歡樂的事情嗎:)
四、OQL高級實例
(測試例子說明)
本篇簡要介紹了PDF.NET V5 版本的功能增強部分,其它實例請看《OQL實例篇》。
4.1,使用星號查詢全部字段
OQL的Select方法如果不傳入任何參數,默認將使用關聯的實體類的全部字段,使用SelectStar 屬性設置“*”進行所有字段的查詢,此特性用於某些情況下不想修改實體類但又想將數據庫表新增的字段查詢到實體類中的情況,比如某些CMS系統,可以讓用戶自由增加文章表的字段。這些增加或者修改的字段,可以通過entity.PropertyList(“fieldName”) 獲取字段的值。
Users user = new Users() { NickName = "pdf.net", RoleID=5 }; OQL q0 = OQL.From(user) .Select() .Where(user.NickName,user.RoleID) .OrderBy(user.ID) .END; q0.SelectStar = true; Console.WriteLine("q0:one table and select all fields \r\n{0}", q0); Console.WriteLine(q0.PrintParameterInfo());
程序輸出:
q0:one table and select all fields SELECT * FROM [LT_Users] WHERE [NickName]=@P0 AND [RoleID]=@P1 ORDER BY [ID] --------OQL Parameters information---------- have 2 parameter,detail: @P0=pdf.net Type:String @P1=5 Type:Int32 ------------------End------------------------
4.2,延遲選取屬性字段
有時候可能會根據情況來決定要Select哪些字段,只需要在OQL實例上多次調用Select方法並傳入實體類屬性參數即可,在最終得到SQL語句的時候才會進行合並處理,實現了延遲選取屬性字段的功能,如下面的例子:
OQL q = OQL.From(user) .Select(user.ID, user.UserName, user.RoleID) .END; q.Select(user.LastLoginIP).Where(user.NickName); Console.WriteLine("q1:one table and select some fields\r\n{0}", q); Console.WriteLine(q.PrintParameterInfo());
程序輸出:
q1:one table and select some fields SELECT [LastLoginIP], [RoleID], [UserName], [ID] FROM [LT_Users] WHERE [NickName]=@P0 --------OQL Parameters information---------- have 1 parameter,detail: @P0=pdf.net Type:String ------------------End------------------------
可以看出,最后輸出的是兩次Select的結果。
4.3,GroupBy約束
OQL會嚴格按照SQL的標准,檢查在查詢使用了GroupBy子句的時候,Select中的字段是否包含在GroupBy子句中,如果不包含,那么會拋出錯誤結果。某些數據庫可能不會有這樣嚴格的約束,從而使得查詢結果跟SQL標准的預期不一樣,比如SQLite,而在OQL進行這樣的約束檢查,保證了OQL對於SQL標准的支持,使得系統有更好的移植性。
下面是一個倆聯合查詢並分組的例子:
OQL q2 = OQL.From(user) .InnerJoin(roles).On(user.RoleID, roles.ID) .Select(user.RoleID, roles.RoleName) .Where(user.NickName, roles.RoleName) .GroupBy(user.RoleID, roles.RoleName) .OrderBy(user.ID) .END; Console.WriteLine("q2:two table query use join\r\n{0}", q2); Console.WriteLine(q2.PrintParameterInfo());
程序輸出:
q2:two table query use join SELECT M.[RoleID], T0.[RoleName] FROM [LT_Users] M INNER JOIN [LT_UserRoles] T0 ON M.[RoleID] = T0.[ID] WHERE M.[NickName]=@P0 AND T0.[RoleName]=@P1 GROUP BY M.[RoleID], T0.[RoleName] ORDER BY M.[ID] --------OQL Parameters information---------- have 2 parameter,detail: @P0=pdf.net Type:String @P1=role1 Type:String ------------------End------------------------
4.4,多實體Where條件連接查詢
SQL中除了多個表之間的左連接、右連接、內連接等Join連接外,還支持一種通過Where條件進行的多表連接的查詢,這種查詢跟內連接等效。
OQL q3 = OQL.From(user, roles) .Select(user.ID, user.UserName, roles.ID, roles.RoleName) .Where(cmp => cmp.Comparer(user.RoleID, "=", roles.ID) & cmp.EqualValue(roles.RoleName)) .OrderBy(user.ID) .END; Console.WriteLine("q3:two table query not use join\r\n{0}", q3); Console.WriteLine(q3.PrintParameterInfo());
程序輸出:
q3:two table query not use join SELECT M.[ID], M.[UserName], T0.[ID], T0.[RoleName] FROM [LT_Users] M ,[LT_UserRoles] T0 WHERE M.[RoleID] = T0.[ID] AND T0.[RoleName] = @P0 ORDER BY M.[ID] --------OQL Parameters information---------- have 1 parameter,detail: @P0=role1 Type:String ------------------End------------------------
4.5,OQLCompare構造函數和操作符重載
下面的例子演示了新版OQL支持的構造函數,需要使用傳遞OQL參數的重載,同時本例還演示了比較條件的操作符重載,是的代碼有更好的可讀性。
void Test2() { Users user = new Users(); UserRoles roles = new UserRoles() { RoleName = "role1" }; OQL q2 = new OQL(user); q2.InnerJoin(roles).On(user.RoleID, roles.ID); OQLCompare cmp = new OQLCompare(q2); OQLCompare cmpResult = ( cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111") & cmp.EqualValue(roles.RoleName) ) | ( (cmp.Comparer(user.UserName, "=", "CDE") & cmp.Property(user.Password) == "222" & cmp.Comparer(roles.RoleName, "like", "%Role2") ) | (cmp.Property(user.LastLoginTime) > DateTime.Now.AddDays(-1)) ) ; q2.Select().Where(cmpResult); Console.WriteLine("OQL by OQLCompare Test:\r\n{0}", q2); Console.WriteLine(q2.PrintParameterInfo()); }
程序輸出:
OQL by OQLCompare Test: SELECT M.*,T0.* FROM [LT_Users] M INNER JOIN [LT_UserRoles] T0 ON M.[RoleID] = T0.[ID] WHERE ( M.[UserName] = @P0 AND M.[Password] = @P1 AND T0.[RoleName] = @P2 ) OR ( ( M.[UserName] = @P3 AND M.[Password] = @P4 AND T0.[RoleName] LIKE @P5 ) OR M.[LastLoginTime] > @P6 ) --------OQL Parameters information---------- have 7 parameter,detail: @P0=ABC Type:String @P1=111 Type:String @P2=role1 Type:String @P3=CDE Type:String @P4=222 Type:String @P5=%Role2 Type:String @P6=2013/7/28 22:15:38 Type:DateTime ------------------End------------------------
4.6,OQLCompare委托與泛型委托
參見測試程序的Test3()、Test4()、Test5()方法,原理和部分代碼實例已經在3.2 Where迷霧—委托神器 一節中做了詳細說明。
4.7,動態構造查詢條件
下面的例子演示了如何在OQLCompare委托方法中,動態的根據其它附加條件,構造OQLCompare查詢條件,同時也演示了通過Lambda表達式與通過委托方法分別實現動態條件構造的過程,而后者的方式適合在.net2.0 下面編寫委托代碼。
void TestIfCondition() { Users user = new Users() { ID=1, NickName="abc",UserName="zhagnsan",Password="pwd."}; OQLCompareFunc cmpFun = cmp => { OQLCompare cmpResult = null; if (user.NickName != "") cmpResult = cmp.Property(user.AddTime) > new DateTime(2013, 2, 1); if (user.ID > 0) cmpResult = cmpResult & cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111"); return cmpResult; }; OQL q6 = OQL.From(user).Select().Where(cmpFun).END; Console.WriteLine("OQL by 動態構建 OQLCompare Test(Lambda方式):\r\n{0}", q6); Console.WriteLine(q6.PrintParameterInfo()); } void TestIfCondition2() { Users user = new Users() { ID = 1, NickName = "abc"}; 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.Property(user.AddTime) > new DateTime(2013, 2, 1); if (user.ID > 0) cmpResult = cmpResult & cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111"); return cmpResult; }
程序輸出:
OQL by 動態構建 OQLCompare Test(Lambda方式): SELECT [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],[LastLoginTime],[LastLoginIP],[Remarks],[AddTime] FROM [LT_Users] WHERE [AddTime] > @P0 AND [UserName] = @P1 AND [Password] = @P2 --------OQL Parameters information---------- have 3 parameter,detail: @P0=2013/2/1 0:00:00 Type:DateTime @P1=ABC Type:String @P2=111 Type:String ------------------End------------------------
OQL by 動態構建 OQLCompare Test(委托函數方式): SELECT [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],[LastLoginTime],[LastLoginIP],[Remarks],[AddTime] FROM [LT_Users] WHERE [AddTime] > @P0 AND [UserName] = @P1 AND [Password] = @P2 --------OQL Parameters information---------- have 3 parameter,detail: @P0=2013/2/1 0:00:00 Type:DateTime @P1=ABC Type:String @P2=111 Type:String ------------------End------------------------
注意:
在 TestIfCondition 方法中,程序中使用了實體類來做if 語句的條件,但是這個實體類是OQL關聯的實體類,在使用實體類屬性的時候會觸發OQL字段堆棧操作。早期版本的PDF.NET SOD框架對此問題支持不是很完善,有可能生成不是預期的SQL語句。該現象在VS的單步調試運行中出現的可能性比較大,這就是以前說的“調試陷阱”。有可能請使用動態查詢條件用戶,請升級到版本 Ver5.2.3.0429 之后的新版本。
下面再給一個例子:
SalesOrder model = new SalesOrder(); model.iOrderTypeID = "123"; //string orderTypeID = model.iOrderTypeID; BCustomer bCustomer = new BCustomer(); OQLCompareFunc<BCustomer,SalesOrder> cmpFun = (cmp,C,S) => { OQLCompare cmpResult = null; cmpResult = cmp.Comparer(S.iBillID, OQLCompare.CompareType.Equal, 1); if (!string.IsNullOrEmpty(S.iOrderTypeID)) cmpResult = cmpResult & cmp.Comparer(S.iOrderTypeID, OQLCompare.CompareType.Equal, S.iOrderTypeID); int iCityID = 39; //由於調用了關聯實體類的 S.iOrderTypeID 用於條件比較,所以下面需要調用 cmp.NewCompare() //cmpResult = cmpResult & cmp.NewCompare().Comparer<int>(C.iCityID, OQLCompare.CompareType.Equal, iCityID); //感謝網友 紅楓星空 發現此問題 //或者繼續采用下面的寫法,但是必須確保 Comparer 方法第一個參數調用為實體類屬性,而不是待比較的值 cmpResult = cmpResult & cmp.Comparer(C.iCityID, OQLCompare.CompareType.Equal, iCityID); return cmpResult; }; OQL oQL = OQL.From(model) .LeftJoin(bCustomer).On(model.iCustomerID, bCustomer.ISID) .Select() .Where(cmpFun) .OrderBy(model.iBillID, "desc") .END; Console.WriteLine(oQL); Console.WriteLine(oQL.PrintParameterInfo()); Console.ReadLine();
注意:上面的變量 iCityID 不能等於屬性 C.iCityID 的當前值,比如0,這種情況框架無法判斷方法使用的實體類屬性是在本方法的參數上,還是方法調用前曾經使用過但還沒有清理過的實體類屬性調用。每當執行了Comparer 方法后,OQL的字段堆棧會清空的,但是這個例子中,它可能沒有被清空,從而有可能出錯。當然,這里還可以采用 調用 NewCompare 方法的方式,見注釋。
正確的輸出結果,應該是:
SELECT M.*,T0.* FROM [tb_SalesOrder] M LEFT JOIN [tb_BCustomer] T0 ON M.[iCustomerID] = T0.[ISID] WHERE M.[iBillID] = @P0 AND M.[iOrderTypeID] = @P1 AND T0.[iCityID] = @P2 ORDER BY M.[iBillID] desc --------OQL Parameters information---------- have 3 parameter,detail: @P0=1 Type:Int32 @P1=123 Type:String @P2=39 Type:Int32 ------------------End------------------------
備注:如果需要了解更多的OQL動態條件查詢的信息,請參考這篇文章《左求值表達式,堆棧,調試陷阱與ORM查詢語言的設計》
4.8,IN 條件子查詢
下面的例子使用一個child 的OQL實例作為q的OQL實例的子對象,構造了一 個IN 條件子查詢。當前實例演示的是簡單子查詢,它沒有在子查詢中引用父查詢的字段。
void TestChild() { Users user = new Users(); UserRoles roles = new UserRoles(); OQL child = OQL.From(roles) .Select(roles.ID) .Where(p => p.Comparer(roles.NickName, "like", "%ABC")) .END; OQL q = OQL.From(user) .Select(user.ID,user.UserName) .Where(cmp => cmp.Comparer(user.RoleID, "in", child)) .END; Console.WriteLine("OQL by 子查詢Test:\r\n{0}", q); Console.WriteLine(q.PrintParameterInfo()); }
程序輸出:
OQL by 子查詢Test: SELECT [ID], [UserName] FROM [LT_Users] WHERE [RoleID] IN (SELECT [ID] FROM [LT_UserRoles] WHERE [RoleNickName] LIKE @P0 ) --------OQL Parameters information---------- have 1 parameter,detail: @P0=%ABC Type:String ------------------End------------------------
4.9,高級子查詢
高級子查詢必須使用OQLChildFunc 委托,並且使用OQL.From(OQL parent,EntityBase entity) 的重載,通過該方式即可在子查詢中使用父查詢的實體類,而子查詢最后作為OQLCompare對象的條件比較方法的參數傳入,即下面代碼中的
cmp.Comparer(user.RoleID, "=", childFunc)
下面是詳細代碼:
void TestChild2() { /* SELECT * FROM [LT_Users] WHERE RoleID = (SELECT ID FROM dbo.LT_UserRoles r WHERE [LT_Users].NickName=r.NickName) */ Users user = new Users() { NickName="_nickName"}; UserRoles roles = new UserRoles() { NickName="_roleNickName"}; OQLChildFunc childFunc = parent => OQL.From(parent,roles) .Select(roles.ID) .Where(cmp => cmp.Comparer(user.NickName, "=", roles.NickName) //比較的字段順序無所謂 & cmp.Property(roles.AddTime) > DateTime.Now.AddDays(-3)) .END; OQL q = OQL.From(user) .Select() .Where(cmp => cmp.Comparer(user.RoleID, "=", childFunc)) .END; q.SelectStar = true; Console.WriteLine("OQL by 高級子查詢Test:\r\n{0}", q); Console.WriteLine(q.PrintParameterInfo()); }
程序輸出:
OQL by 高級子查詢Test: SELECT * FROM [LT_Users] M WHERE [RoleID] = (SELECT [ID] FROM [LT_UserRoles] WHERE M.[NickName] = [RoleNickName] AND [AddTime] > @P0 ) --------OQL Parameters information---------- have 1 parameter,detail: @P0=2013/7/26 22:15:38 Type:DateTime ------------------End------------------------
4.10,批量更新操作
下面的例子使用了Lambda 條件方式的Where作為更新的條件,在被注釋的代碼中,還演示了舊版本的條件更新方式。如果更新條件對應的數據不是單條的,那么即可實現“批量更新”的效果。
void TestUpdate() { Users user = new Users() { AddTime=DateTime.Now.AddDays(-1), Authority="Read", NickName = "菜鳥" }; OQL q = OQL.From(user) .Update(user.AddTime, user.Authority, user.NickName) .Where(cmp => cmp.Property(user.RoleID) == 100) .END; //OQL q = OQL.From(user) // .Update(user.AddTime) // .Where(user.Authority, user.NickName) // .END; Console.WriteLine("OQL update:\r\n{0}\r\n",q); Console.WriteLine(q.PrintParameterInfo()); }
程序輸出:
OQL update: UPDATE [LT_Users] SET [AddTime] = @P0, [Authority] = @P1, [NickName] = @P2 WHERE [RoleID] = @P3 --------OQL Parameters information---------- have 4 parameter,detail: @P0=2013/7/28 22:15:38 Type:DateTime @P1=Read Type:String @P2=菜鳥 Type:String @P3=100 Type:Int32 ------------------End------------------------
4.11,動態排序
有時候我們需要根據用戶的選擇來決定派系的方式和排序的字段,這個時候就需要查詢具有動態排序功能了,只需要在OQL的OrderBy方法內調用一個排序委托方法即可。下面的例子中被注釋的部分,總共演示了OQL支持的3種排序方式。
void TestOQLOrder() { Users user = new Users(); //OQLOrderAction<Users> action = this.OQLOrder; OQL q = OQL.From(user) .Select(user.UserName,user.ID) //.OrderBy(p => p.Desc(user.UserName).Asc(user.ID)) //.OrderBy(action,user) .OrderBy<Users>(OQLOrder,user) //3種OQLOrder 對象的使用方法 .END; Console.WriteLine("OQL test OQLOrder object:\r\n{0}\r\n", q); } void OQLOrder(OQLOrder p, Users user) { p.Desc(user.UserName).Asc(user.ID); }
程序輸出:
OQL test OQLOrder object: SELECT [UserName], [ID] FROM [LT_Users] ORDER BY [UserName] DESC, [ID] ASC
4.12,批量數據插入
新版本支持通過OQL進行實體類的數據插入,同時還支持高效的直接從數據庫的查詢結果插入目標表的操作。前者直接使用OQL的Insert方法,后者使用InsertFrom方法。示例只插入了一列數據,如果需要插入多列,在確保子查詢返回多列的情況下,用下面的方式:
OQL q = OQL.From(user)
.InsertFrom(child,user.RoleID,user.ID,user.Name,user.NickName);
下面是實際的例子:
void TestInsert() { Users user = new Users() { AddTime = DateTime.Now.AddDays(-1), Authority = "Read", NickName = "菜鳥" }; OQL q = OQL.From(user) .Insert(user.AddTime, user.Authority, user.NickName); Console.WriteLine("OQL insert:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo()); } void TestInsertFrom() { Users user = new Users(); UserRoles roles = new UserRoles(); OQL child = OQL.From(roles) .Select(roles.ID) .Where(cmp => cmp.Comparer(roles.ID, ">", 100)) .END; OQL q = OQL.From(user) .InsertFrom(child,user.RoleID); Console.WriteLine("OQL insert from:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo()); }
程序輸出:
OQL insert: INSERT INTO [LT_Users] ( [AddTime], [Authority], [NickName]) VALUES (@P0,@P1,@P2) --------OQL Parameters information---------- have 3 parameter,detail: @P0=2013/7/28 22:15:38 Type:DateTime @P1=Read Type:String @P2=菜鳥 Type:String ------------------End------------------------ OQL insert from: INSERT INTO [LT_Users] ( [RoleID] ) SELECT [ID] FROM [LT_UserRoles] WHERE @P0 > [ID] --------OQL Parameters information---------- have 1 parameter,detail: @P0=0 Type:Int32 ------------------End------------------------
4.13,指定查詢的鎖定方式
SqlServer可以在SQL單條查詢語句中指定查詢的鎖定方式,比如行鎖、頁鎖或者不鎖定數據等,詳細內容可以參考OQL.SqlServerLock 枚舉類型定義,或者參考SqlServer聯機幫助。
請注意:如果使用了OQL的With方法指定了查詢的鎖定方式,那么該條OQL將只能在SqlServer中使用,不利於OQL的跨數據庫平台的特性,但由於PDF.NET用戶的強烈要求,最終加入了該特性。實際上,對查詢的鎖定方式,可以通過指定事務的隔離級別實現。
下面的例子實現了對User表查詢的 NOLOCK :
void TestSqlLock() { Users user = new Users(); OQL q = OQL.From(user) //.With(OQL.SqlServerLock.NOLOCK) .With("nolock") .Select(user.ID,user.UserName,user.NickName) .END; Console.WriteLine("OQL Test SQL NoLock:\r\n{0}\r\n", q); }
程序輸出:
OQL Test SQL NoLock: SELECT [ID], [UserName], [NickName] FROM [LT_Users] WITH(NOLOCK)
4.14,字段的計算條件
不同於2個字段之間的簡單比較,有時候可能需要1個字段進行計算后,再跟第2個字段比較,這種條件我們稱呼它為“計算條件”。實際上,對1個字段的計算有點類似於對字段使用函數的操作,只是這個“函數”是個匿名函數而已。比如有下面的查詢條件:
user.LastLoginTime-user.AddTime>'23:00:00'
比較最后登錄時間與用戶記錄增加的時間要大於23小時(當然這個條件可以通過DateDiff函數來實現,這里只是用它來做一個例子說明計算條件),我們使用“不等式替換”原理,上面的條件可以改寫為:
user.LastLoginTime -'23:00:00'>user.AddTime
於是,這個計算條件,就可以使用OQLCompare的“函數條件表達式”了,下面之間給出OQL的例子:
Users user = new Users(); // user.LastLoginTime-user.AddTime>'23:00:00' // => user.LastLoginTime -'23:00:00'>user.AddTime OQL q = OQL.From(user) .Select() .Where(cmp => cmp.Comparer(user.LastLoginTime, ">", user.AddTime, "{0}-'23:00:00'")) .END; q.SelectStar = true; Console.WriteLine("OQL Test SQL Field compute:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo());
程序輸出:
OQL Test SQL Field compute: SELECT * FROM [LT_Users] WHERE [LastLoginTime]-'23:00:00' > [AddTime] -------No paramter.--------
程序成功達到我們的預期,這說明,只要我們肯思考,問題還是容易解決的。
(注:該小結內容於2013.8.7日增加)
4.15,NOT 邏輯比較條件
SQL的NOT操作用於對條件表達式取反,盡管對於“相等”可以取反得到“不等”操作,但對於比較復雜的組合條件,整體取反從邏輯語義上來說,更容易理解。所以OQL也支持NOT邏輯比較條件。只需要使用OQLCompare.Not() 方法即可,比如有下面的查詢語句:
Users user = new Users(); OQL q = OQL.From(user) .Select(user.ID, user.UserName,user.Password) .Where<Users>((cmp, u) => OQLCompare.Not( cmp.Property(u.UserName) == "ABC" & cmp.Property(u.Password) == "123") ) .END; Console.WriteLine("OQL Test NOT Condition:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo());
程序輸出:
OQL Test NOT Condition: SELECT [ID], [UserName], [Password] FROM [LT_Users] WHERE NOT ( [UserName] = @P0 AND [Password] = @P1 ) --------OQL Parameters information---------- have 2 parameter,detail: @P0=ABC Type:String @P1=123 Type:String ------------------End------------------------
4.16,BETWEEN操作
Between 用於指定條件比較的范圍,是包含關系,相當於 min <= x <=max 。用Between可以簡化這樣的條件比較。
OQLCompare的Between方法是這樣定義:
/// <summary> /// 指定條件的包含范圍 /// </summary> /// <typeparam name="T">屬性字段的類型</typeparam> /// <param name="field">屬性字段</param> /// <param name="beginValue">起始值</param> /// <param name="endValue">結束值</param> /// <returns>比較對象</returns> public OQLCompare Between<T>(T field, T beginValue, T endValue) { //略 }
只需要這樣使用:
OQL q7 = OQL.From(user).Select() .Where(cmp =>cmp.Between(user.ID,5,10)) .END; q7.SelectStar = true; Console.WriteLine("q7:having Test: \r\n{0}", q7); Console.WriteLine(q7.PrintParameterInfo());
程序輸出:
q7:having Test: SELECT * FROM [LT_Users] WHERE [ID] BETWEEN @P0 AND @P1 --------OQL Parameters information---------- have 2 parameter,detail: @P0=5 Type:Int32 @P1=10 Type:Int32 ------------------End------------------------
4.17 使用SQL函數進行比較
有時候,我們可能需要對一個字段進行一個SQL函數計算,然后再讓這個結果跟某一個值進行比較,當然這些函數可能在不同的數據庫中是不同的,比如SqlServer與Oracle在很多字段處理函數上都不同,下面以SqlServer 求取某個字段的小時數是否大於15點:
Users user = new Users(); OQL q = OQL.From(user) .Select() .Where(cmp => cmp.ComparerSqlFunction(user.LastLoginTime, ">", 15, "DATEPART(hh, {0})")) .END; q.SelectStar = true; Console.WriteLine("OQL Test SQL Fuction:\r\n{0}\r\n", q); Console.WriteLine(q.PrintParameterInfo());
程序輸出:
OQL Test SQL Fuction: SELECT * FROM [LT_Users] WHERE DATEPART(hh, [LastLoginTime]) > @P0 --------OQL Parameters information---------- have 1 parameter,detail: @P0=15 Type:Int32 ------------------End------------------------
通過這種方式,我們能夠在比較條件上應用任何SQL函數,相比EF,這種方式要簡單。注意這里使用的是 OQLCompare的 ComparerSqlFunction 函數。
附錄
下面是PDF.NET Ver5.0版本OQL測試完整的源碼和SQL輸出,其中大部分內容已經在上面的章節中做過說明,但有少部分未在正文中做說明,供大家集中參考。
附錄相關的程序將在PDF.NET的開源項目 http://pwmis.codeplex.com 下載頁面提供下載, PDF.NET_V5.0_Beta_20130807 (已經更新,之前下載過的請重新下載)
有關框架更多的信息,請參考框架官網 http://www.pwmis.com/sqlmap 。
附錄1:OQL測試完整源碼
附錄2:OQL測試程序輸出的SQL信息
