ORM查詢語言(OQL)簡介--高階篇:脫胎換骨
相關文章內容索引:
在寫本文之前,一直在想文章的標題應怎麼取。在寫了《ORM查詢語言(OQL)簡介--概念篇》、《ORM查詢語言(OQL)簡介--例項篇》之後,覺得本篇文章應該是前2篇的延續,但又不是一般的延續,因為今天要寫的這篇內容,是基於對框架OQL完全重構之後來寫的,所以加上一個副標題:脫胎換骨!
一、OQL之前生
1.1,內容回顧:
OQL是我設計用來處理PDF.NET開發框架的ORM查詢的,因此叫做ORM查詢語言。自2006年第一版以來,經歷了多次重構,到PDF.NET Ver 4.X 版本,已經比較穩定了,在我做的專案和框架使用者朋友的專案中得到成功應用,基本符合一般的常規應用需求。
OQL有下面3個顯著特點:
- 抽象的SQL,遮蔽了具體資料庫的差異,因此支援所有資料庫;
- 物件化的“SQL”,寫OQL程式碼能夠獲得IDE的智慧提示,能夠得到編譯時檢查確保不會寫出錯誤的SQL;
- 沒有使用.NET的特性,比如泛型、反射、表示式樹等東西,因此理論上OQL可以跨語言平臺,比如移植到Java,C++,VB等。
OQL的原理基於2大特性:
- 表示式的鏈式呼叫
- 屬性的例項呼叫
OQL支援4大類資料操作
- 資料查詢:
- 單實體類(單表)查詢
- 多實體類(多表)關聯查詢
- 資料修改
- 更新資料
- 刪除資料
- 統計、聚合運算
- OQL分頁
1.2,老版本的侷限
儘管OQL已經可以解決80%的查詢需求,剩下的20%查詢需求我都建議框架使用者使用SQL-MAP技術來完成,但對於使用者而言,是不太願意從ORM模式切換到SQL模式的,希望OQL能夠解決儘可能多的查詢需求。那麼,PDF.NET Ver 4.X 版本的OQL有哪些不足呢?
1.2.1,自連線查詢
也稱為表自身連線查詢。OQL支援多表(實體)查詢的,但卻無法支援自連線查詢,原因是自連線查詢必須指定表的別名:
SELECT R1.readerid,R1.readername,R1.unit,R1.bookcount FROM ReaderInfo ASR1,ReaderInfo AS R2 WHERE R2.readerid=9704 AND R1.bookcount>R2.bookcount --連線關係 ORDER BY R1.bookcount
上面這個查詢是從ReaderInfo表查詢可借圖書數目比編號為9704讀者多的所有讀者資訊,這裡對錶使用了別名來實現的,如果不使用別名,那麼這個查詢就無法實現。而OQL之前的版本,是不支援表的別名的,因此,對於連線查詢,OQL生成的可能是這樣子的SQL語句:
SELECT teacher.*,student.* FROM teacher INNER JOIN student ON teacher.id=student.tea_id
1.2.2,子查詢
老版本OQL僅支援IN條件的子查詢,不能像SQL那麼靈活的進行各種子查詢,其實不支援的原因其中一個也是因為OQL查詢不支援表的別名,另外一個原因是子查詢無法獲取到父查詢的表名和欄位名。子查詢是一個很常用的功能,如果不能夠支援,那麼就大大限制了OQL的使用範圍。
下面是來自SQLSERVER 聯機幫助的說明:
子查詢也稱為內部查詢或內部選擇,而包含子查詢的語句也稱為外部查詢或外部選擇。
許多包含子查詢的 Transact-SQL 語句都可以改用聯接表示。其他問題只能通過子查詢提出。在 Transact-SQL 中,包含子查詢的語句和語義上等效的不包含子查詢的語句在效能上通常沒有差別。但是,在一些必須檢查存在性的情況中,使用聯接會產生更好的效能。否則,為確保消除重複值,必須為外部查詢的每個結果都處理巢狀查詢。所以在這些情況下,聯接方式會產生更好的效果。以下示例顯示了返回相同結果集的 SELECT 子查詢和 SELECT 聯接:
/* SELECT statement built using a subquery. */ SELECT Name FROM AdventureWorks2008R2.Production.Product WHERE ListPrice = (SELECT ListPrice FROM AdventureWorks2008R2.Production.Product WHERE Name = 'Chainring Bolts' ); /* SELECT statement built using a join that returns the same result set. */ SELECT Prd1. Name FROM AdventureWorks2008R2.Production.Product AS Prd1 JOIN AdventureWorks2008R2.Production.Product AS Prd2 ON (Prd1.ListPrice = Prd2.ListPrice) WHERE Prd2. Name = 'Chainring Bolts';
1.2.3,OQL資料插入
儘管OQL可以支援實體類的批量更新與刪除,但沒有支援實體類的插入,原因是對單個實體類而言,可以直接呼叫EntityQuery<T>的Insert()方法實現。但專案中可能還是有需要寫SQL插入資料的情況,比如插入Int型別的值為0,如果用實體類的方式那麼該列不會被插入,因為PDF.NET的實體類認為該屬性值沒有改變,PDF.NET的插入和更新操作,都只處理“屬性值改變過的”資料。另外,也不支援Insert....Select....From 這種批量插入方式。
INSERT INTO MySalesReason SELECT SalesReasonID, Name, ModifiedDate FROM AdventureWorks2008R2.Sales.SalesReason WHERE ReasonType = N'Marketing';
1.2.4,HAVING 子句
OQL老版本不支援該功能,儘管不是很常用,但配合分組查詢還是有用的,如下面的例子HAVING
子句從 SalesOrderDetail
表中檢索超過 $100000.00
的每個 SalesOrderID
的總計。
SELECT SalesOrderID, SUM(LineTotal) AS SubTotal FROM Sales.SalesOrderDetail GROUP BY SalesOrderID HAVING SUM(LineTotal) > 100000.00 ORDER BY SalesOrderID ;
二、浴火重生
現在流行的ORM框架很多,剛剛在寫本文的時候,還發現有個朋友寫了一篇各ORM對比測試的文章:
這麼多ORM框架,我並不是很熟悉,PDF.NET的目標只想在某些方面趕超MS的EF框架,據說現在EF6都快出來了,EF4.5在效能上上了一個臺階。面對EF這個強敵,如果PDF.NET不能解決前面說的幾大缺陷,註定距離會越來越遠,PDF.NET的使用者對我也是常常提出批評,紛紛轉投EF去了,對此我深感壓力山大!
儘管EF是PDF.NET ORM 的強勁對手,但 PDF.NET ORM的查詢語言OQL,相對於EF的查詢語言Linq,還是有自己獨立的特色,OQL比Linq更接近SQL,Linq是VS的語法糖,本質上VS編譯器會將它轉化成Lambda表示式,進一步轉換成表示式樹,最後翻譯成SQL語句交給資料庫去執行。所以我們會看到針對集合操作的擴充套件方法,有很多都要使用 => 的呼叫方式,而OQL沒有使用Lambda,它是怎麼獲取到查詢對應的表名稱和欄位名稱的呢?它是怎麼實現SQL查詢的層次結構的呢?
2.1,屬性欄位的祕密
PDF.NET屬性的定義採用下面的形式:
public System.String UserName { get { return getProperty<System.String>("UserName"); } set { setProperty("UserName", value, 50); } }
因此在獲取實體類的屬性值的時候,呼叫了getProperty<T>("欄位名") 這個方法,它裡面會觸發屬性讀取事件:
/// <summary> /// 獲取屬性值 /// </summary> /// <typeparam name="T">值的型別</typeparam> /// <param name="propertyName">屬性名稱</param> /// <returns>屬性值</returns> protected T getProperty<T>(string propertyName) { this.OnPropertyGeting(propertyName); return CommonUtil.ChangeType<T>(PropertyList(propertyName)); } /// <summary> /// 屬性獲取事件 /// </summary> public event EventHandler<PropertyGettingEventArgs> PropertyGetting; /// <summary> /// 獲取屬性的時候 /// </summary> /// <param name="name"></param> protected virtual void OnPropertyGeting(string name) { if (this.PropertyGetting != null) { this.PropertyGetting(this, new PropertyGettingEventArgs(name)); } }
/// <summary> /// 屬性獲取事件 /// </summary> public class PropertyGettingEventArgs : EventArgs { private string _name; /// <summary> /// 屬性名稱 /// </summary> public string PropertyName { get { return _name; } set { _name = value; } } /// <summary> /// 以屬性名稱初始化本類 /// </summary> /// <param name="name"></param> public PropertyGettingEventArgs(string name) { this.PropertyName = name; } }PropertyGettingEventArgs
而OQL例項物件,正是訂閱了EventHandler<PropertyGettingEventArgs> 事件:
public OQL(EntityBase e) { currEntity = e; //其它程式碼略 e.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(e_PropertyGetting); } void e_PropertyGetting(object sender, PropertyGettingEventArgs e) { //其它程式碼略 }
所以,在屬性獲取事件中,我們可以通過PropertyGettingEventArgs.PropertyName 得到實體類屬性對應的欄位名稱,因此,我們就可以方便的做到選取我們本次查詢需要的欄位,例如下面的OQL查詢:
Users user = new Users(); OQL q0 = OQL.From(user) .Select(user.ID, user.UserName, user.RoleID) .END;
對應的SQL語句:
SELECT [ID], [UserName], [RoleID] FROM [LT_Users]
這樣,我們無需使用委託,也不需要Lambda表示式,更不需要表示式樹,就能夠直接獲取到要查詢的表名稱和欄位名稱,寫法比Linq更簡潔,處理速度更快速。當然,代價是要先例項化實體類。
2.1,屬性獲取事件的變化
在事件方法e_PropertyGetting 中,我們看看PDF.NET Ver 5.0前後的變化:
Ver 4.X 以前:
void e_PropertyGetting(object sender, PropertyGettingEventArgs e) { doPropertyGetting(((EntityBase)sender).TableName, e.PropertyName); } private void doPropertyGetting(string tableName, string propertyName) { if (isJoinOpt) { string propName = "[" + tableName + "].[" + propertyName + "]"; this.currJoinEntity.AddJoinFieldName(propName); } else { string field = this.joinedString.Length > 0 ? tableName + "].[" + propertyName : propertyName; if (!hasSelected && !selectedFields.Contains(field)) selectedFields.Add(field); } this.getingTableName = tableName; this.getingPropertyName = propertyName; }
在doPropertyGetting 方法中,區分是否有實體類連線查詢,來處理不同的表名稱和欄位名稱,這裡看到連線查詢的時候沒有為表加上別名,而是直接使用了“表名稱.欄位名稱”這種表示欄位的形式。同時,將當前獲取到的表字段名,馬上賦值給getingPropertyName 變數。這帶來了一個問題,屬性欄位名稱必須馬上被使用,否則就會出問題。
由於不同的情況使用屬性欄位的時機不一樣,為了處理這些不同的情況加入了各種Case下的處理程式碼,比如將Select方法要使用的屬性欄位名稱儲存到列表 selectedFields 中。這種處理方法無疑大大增加了程式碼的複雜度。
Ver 5.0 版本的改進
前面說到屬性獲取到的屬性欄位名稱必須馬上被使用,否則就會出問題。如果我們不論何種情況,都將這個屬性欄位名先儲存起來再使用呢?使用佇列?連結串列?堆疊?這些集合都可以,但在編譯原理中,對錶達式的處理都是使用堆疊來做的,其中必有它的好處,以後會體會到。
下面是屬性獲取事件程式碼:
void e_PropertyGetting(object sender, PropertyGettingEventArgs e) { TableNameField tnf = new TableNameField() { Field = e.PropertyName, Entity = (EntityBase)sender, Index=this.GetFieldGettingIndex() }; fieldStack.Push(tnf); }
這裡直接將屬性欄位名存在TablenameField 結構的Field欄位中,然後將這個結構壓入堆疊物件fieldStack 中,需要的時候在從堆疊中彈出最新的一個 TableNameField 結構。這樣,不論是OQL的Select方法,Where方法還是OrderBy方法,都能夠使用統一的堆疊結構來獲取方法使用的屬性欄位了。
2.3,統一屬性獲取事件
除了OQL本身需要“屬性獲取事件”,OQL關聯的OQLCompare物件,OQLOrder物件,都需要處理屬性獲取事件,比如之前例項化OQLCompare物件:
/// <summary> /// 使用一個實體物件初始化本類 /// </summary> /// <param name="e"></param> public OQLCompare(EntityBase e) { this.CurrEntity = e; //this.CurrEntity.ToCompareFields = true; this.CurrEntity.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(CurrEntity_PropertyGetting); } /// <summary> /// 使用多個實體類進行連線查詢的條件 /// </summary> /// <param name="e"></param> /// <param name="joinedEntitys"></param> public OQLCompare(EntityBase e, params EntityBase[] joinedEntitys) { this.CurrEntity = e; this.CurrEntity.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(CurrEntity_PropertyGetting); //處理多個實體類 if (joinedEntitys != null && joinedEntitys.Length > 0) { this.joinedEntityList = new List<EntityBase>(); foreach (EntityBase item in joinedEntitys) { this.joinedEntityList.Add(item); item.PropertyGetting += new EventHandler<PropertyGettingEventArgs>(CurrEntity_PropertyGetting); } } }
屬性獲取事件處理方法:
void CurrEntity_PropertyGetting(object sender, PropertyGettingEventArgs e) { if (this.joinedEntityList != null) { this.currPropName = "[" + ((EntityBase)sender).TableName + "].[" + e.PropertyName + "]"; } else { this.currPropName = "[" + e.PropertyName + "]"; //propertyList.Add(e.PropertyName); } }
之所以要在OQLCompare等物件中也要處理屬性獲取事件,是為了OQLCompare能夠獨立使用,但這帶來一些問題:
- 各地的屬性獲取事件處理程式碼類似,程式碼有冗餘;
- 沒有體現出OQL跟OQLCompare 、OQLOrder物件之見的聚合性,呈現出鬆散的結構,因此可能出現OQLCompare使用的實體類在OQL中沒有使用,從而產生錯誤的查詢;
- OQLCompare中的的欄位名與OQL缺乏相關性,因此只能通過“表名稱.欄位名稱”這種形式來使用屬性欄位名,無法使用別名。
Ver 5.0的解決辦法:
在OQL物件上,定義一些方法供OQL的關聯子物件來訪問需要的屬性欄位名資訊:
/// <summary> /// 從堆疊上只取一個欄位名 /// </summary> /// <returns></returns> protected internal string TakeOneStackFields() { //其它程式碼略 TableNameField tnf = fieldStack.Pop(); return GetOqlFieldName(tnf); }
2.4,SQL的語法結構
SQL是結構化查詢語言,它自身也是非常結構化的,每一個查詢都有固定的語法結構,以Select為例,它可以有多種形式的寫法:
SELECT Field1,Field2... FROM [Table] ----------------- SELECT Field1,Field2... FROM [Table] WHERE Condition ----------------- SELECT Field1,Field2... FROM [Table] WHERE Condition ORDER BY FieldOrder ----------------- SELECT FieldGroup FROM [Table] WHERE Condition GROUP BY FieldGroup ORDER BY FieldOrder ---------------- SELECT FieldGroup FROM [Table] WHERE Condition GROUP BY FieldGroup HAVING havingExp ORDER BY FieldOrder ----------------- SELECT Field1,Field2... FROM [Table1] JOIN [Table2] ON [Table1].PK=[Table2].FK WHERE Condition ORDER BY FieldOrder ----------------- SELECT Field1,Field2... FROM [Table1],[Table2] WHERE [Table1].PK=[Table2].FK And OtherCondition ORDER BY FieldOrder -----------------
仔細觀察,萬變不離其宗,上面的寫法其實都是以下關鍵字所在層次結構在不同情況下的組合而已,除了Select是必須的:
SELECT FROM JOIN ON WHERE GROUP BY HAVEING ORDER BY
2.5,OQL的層次結構
如果要以面向物件的方式來實現SQL這個關鍵字層次結構,我們必須將相關的關鍵字作為方法,定義在合適的物件中,然後靠物件的層次結構,來限定正確的“SQL”結構,為此,我們先重新來定義一下OQL使用的介面IOQL和關聯的介面的層次定義:
public interface IOQL { OQL1 Select(params object[] fields); } public interface IOQL1 : IOQL2 { //OQL End { get; } //OQL3 GroupBy(object field); //OQL4 Having(object field); //OQL4 OrderBy(object field); OQL2 Where(params object[] fields); } public interface IOQL2 : IOQL3 { //OQL End { get; } OQL3 GroupBy(object field); //OQL4 Having(object field); //OQL4 OrderBy(object field); } public interface IOQL3 : IOQL4 { OQL4 Having(object field); //OQL End { get; } //OQL4 OrderBy(object field); } public interface IOQL4 { OQL END { get; } OQLOrderType OrderBy(object field); }
然後,讓OQL實現IOQL,在定義其他OQL子物件來實現其它子介面。由於物件比較多,還是通過一個物件結構圖來看更方便:
圖1:OQL介面層次圖
圖2:OQL體系結構圖
2.6 OQLCompare--比較物件的組合模式
SQL的查詢條件可以很簡單,也可以很複雜,比如下面的複合查詢條件:
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.[ID] IN (1,2,3) ) OR M.[LastLoginTime] > @P5 )
這個查詢條件分為2組條件,然後第二組查詢內部又包含2組查詢,從括號層數來說,僅僅有3層,但看起來已經夠複雜了。實際專案中,我曾遇到過用5000行業務程式碼來構造SQL查詢條件的情況,不要吃驚,的確是5000行業務程式碼,當然不是說SQL條件有5000行,但也可以想象到,最終生成的SQL查詢條件的長度不會小於50行。這樣複雜的查詢條件,如果用拼接SQL字串的方式來完成,工作量是不可想象的,維護起來也是非常困難。但是,我們可以利用OQL的查詢條件物件OQLCompare來完成,因為它實質上是一個組合物件,即N多個OQLCompare組合成一個OQLCompare物件,不過為了實現方便,我們規定每個OQLCompare物件下面存放2個子物件,也就是建立一個二叉樹來儲存所有的比較物件:
public class OQLCompare { //其它程式碼略 protected OQLCompare LeftNode { get; set; } protected OQLCompare RightNode { get; set; } protected CompareLogic Logic { get; set; } protected bool IsLeaf { get { return object.Equals(LeftNode, null) && object.Equals(RightNode, null); } } protected string ComparedFieldName; protected string ComparedParameterName; protected CompareType ComparedType; }
還是用一張圖來看看查詢條件的構成比較直觀:
圖3:OQLCompare 物件樹
該圖的內容,說明了構造上面的SQL條件的OQLCompare比較物件的樹型結構,我們規定,每個節點下面只有左節點和右節點,左節點優先,左右子節點都可以是空,如果符合該條件,則當前節點為葉子結點,否則為枝節點。
從上圖可以很容易發現,其實這就是一個“組合模式”,而組合模式的每個節點都具有相同的行為和特性,所以,我們可以構建非常複雜的組合體系,最終構造超級複雜的查詢條件,而在最終使用上,一組查詢條件跟一個查詢條件的處理過程是一樣的。
2.7,條件表示式的括號問題
括號是控制表示式計算順序的重要手段,對於邏輯表示式,使用AND,OR 來連線兩個子表示式,如果AND,OR同時出現,則需要用括號來改變表示式元素計算的順序。C,C++,C# 對錶達式都是“左求值計算”的,這是一個很重要的概念,某些程式語言可能是“右求值計算”的。如果表示式中有括號,那麼前面的計算將掛起,計算完括號內的結果後,再繼續處理表達式的剩餘部分。因此,我們可以把括號看作一個“樹枝節點”,而括號內最內層的節點,為葉子結點,按照我們對節點型別的定義和上面示例的OQLCompare條件組合樹,在輸出SQL條件字串的時候,可能是這個樣子的:
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.[ID] IN (1,2,3) ) OR M.[LastLoginTime] > @P5 ) )
假設條件表示式需要對10個欄位的比較內容進行AND 判斷,那麼將會巢狀10-1=9 層括號。
不要小看這個問題,前面我說到的那個5000行業務程式碼構建SQL查詢條件的事情,就曾經發生過構造了128層括號的事情,最終導致SQLSERVER報錯:
查詢條件括號巢狀太多,查詢分析器無法處理!
那麼括號怎麼化簡呢?
這個得從表示式的邏輯語義上去分析:
- (A AND B) AND C <==> A AND B AND C
- (A OR B) OR C <==> A OR B OR C
- (A AND B) AND (C AND D) <==> A AND B AND C AND D
- (A OR B) OR (C OR D) <==> A OR B OR C OR D
- (A AND B) AND (C AND D) <==> A AND B AND C AND D
- (A OR B) OR C <==> A OR B OR C
所以,我們可以檢查“子樹枝節點”的邏輯比較型別,如果它的型別與當前節點的邏輯比較型別相同,那麼對子樹枝節點的處理就不需要使用括號了。
可以通過哦遞迴過程,處理完所有的子節點的括號問題,從而最終得到我們看起來非常簡單的條件表示式。
(本文篇幅太長,未完待續)