ORM查詢語言(OQL)簡介--高階篇(續):廬山真貌
相關文章內容索引:
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
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; };