【聲明】
寫作不易,轉載請注明出處(
http://www.cnblogs.com/wiseant/p/3985353.html)。
【系列文章】
通用查詢實現方案(可用於DDD)[附源碼] -- 代碼解讀
【前言】
關於DDD我一直也是看得多,做得少,可以說未入門,自然也寫不出什么東西。但是我對DDD相關的思考倒並不少,特別是對於查詢好像沒有一個很優雅的實現方案。
上個月dax.net寫了一篇關於統一查詢實現思路的文章:
一種通用查詢語言的定義與實踐,給了我動力和靈感,感謝dax.net兄。
緣於這個契機,我開始嘗試實現一個通用查詢方案,最近一直在做NC5.7的二次開發,只能用業余時間來搞這個,經過半個多月的努力,實現了一個 自我感覺還算優雅的通用查詢方案。但一個人的智慧始終是有限的,而且本人也是半路出家,底子比較薄,遂將項目開源出來,一方面是希望可以幫助到初學者,以 便更多人來檢驗此查詢方案的通用性和可靠性,另外主要也希望得到園子里大牛們的指點,使此查詢方案得到更一步發展和提煉而日趨成熟。
PS:順便吐槽一下NC-UAP5.7開發平台,說文明點就是太難用了,說得不文明點那就是一坨屎!用友還枉稱什么國內一流的軟件供應商
【功能】
以前一直從事基於數據庫的企業應用開發,用戶經常需要在客戶端靈活的進行數據檢索,我采用的是拼SQL的方式,雖然有各種弊端,但好處是相當自 由。到了DDD發現拼SQL基本上是行不通了,很多開發人員都采用在倉儲中定義類似FindByKey()、FindByName()、 FindByNameAndAge()這樣的方法來提供數據檢索,每當用戶需要多一種條件組合時都要改很多地方,大費周章。我希望這個查詢對象定義可以有 代碼檢查功能,條件組合靈活多樣,可以在前后台之前傳輸,並且一次定義就可以被編譯為不同的目標格式,如SQL、Lambda表達式。
下面分別介紹這些功能如何使用。
一、直觀的定義查詢對象
1)支持以Linq表達式定位屬性

如上圖,首先我們以傳統的方式定義了兩個字段,緊接着我們以Linq表達式方式直接獲取了這兩個字段,經過用單元測試方法來驗證兩種方式所獲取的字段是等同的。
顯然第二種方式更優雅簡潔,更重要的是當字段改名或更改了字段類型時,第一種方式編譯依然可以通過,而運行時將不可避免的產生異常,導致程序維 護難度增加;而第二種方式在遇到字段改名時將在編譯報告錯誤,如果你使用ReSharp之類工具的代碼重構功能的話,更是會自動做出相應修改。
鑒於以上原因,我將在后續示例代碼中僅采用第二種方式書寫,我推薦你也使用第二種方式。
2)支持計算公式
如果僅僅支持對字段的比較顯然是不夠的,我希望日常80%~90%對查詢的需求可以通過此通用查詢方案來解決。
顯然支持計算公式和括號優先級是非常必要的,我們通過下面這張代碼貼圖可以了解一個使用算式的簡單例子。

3)支持導航屬性條件
還有一個很有必要支持的功能是對導航屬性的條件查詢,在關系數據庫中即為關聯表條件查詢。
第一張代碼貼圖中其實已經有相關代碼展示了,我將該代碼行單獨剪切出來放在下圖,圖中代碼行定義一個"訂單.供應商.編碼"字段,后面的查詢示例中會有更詳細的代碼展示。

4)支持括號優先級
上面已經提到過支持括號優先級的必要性,下面的代碼將展示定義一個加法運算,並將這個算式用括號括起來,保證此算式優先計算。

二、查詢對象可序列化及反序列化
無論是DDD還是傳統的三層架構都涉及到分層傳輸的問題,那么我們的通用查詢方案也必須提供序列化及反序列化的功能,通過下面的代碼貼圖可以看到對XML格式文本的序列化支持,由於時間所限我將在稍后提供對json格式的序列化支持。

三、定義好查詢后可同時編譯為SQL或lambda表達式
如果前面的代碼你能讀懂(其實不難),那么下面的代碼有很詳細的注釋,相信對你也沒什么難度,為節省時間,我就不費口舌了。
1 [Fact] 2 public void TestQuery3() 3 { 4 var helper = new TypeInfoHelper<Order>(); 5 //獲得訂單集合中的第一張訂單,用於之后的單元測試驗證 6 var findOrder = orders.First(); 7 //定義一個查詢,條件為訂單主鍵等於前面獲取的訂單對象的主鍵值 8 var query = new Query(typeof(Order)) 9 { 10 RootExpression = helper.GetProperty(p => p.BillId).EqualTo(findOrder.BillId) 11 }; 12 /*第一種編譯方式*/ 13 //定義一個生成Lambda表達式的編譯器對象 14 var compiler = new LambdaExpressionCompiler<Order>(query); 15 //將查詢對象編譯為Linq表達式 16 var expression = compiler.Compile(); 17 //對訂單集合執行linq查詢 18 var items = orders.Where(expression.Compile()).ToList(); 19 //驗證查詢結果中僅包含一張訂單,且訂單編號與參照訂單的編號相同 20 Assert.Equal(items.Count, 1); 21 Assert.Equal(items.First().BillNo, findOrder.BillNo); 22 23 /*第二種編譯方式*/ 24 //定義一個生成SQL腳本的編譯器對象 25 var sqlCompiler = new SqlWhereClauseCompiler(query) { GenSelectPart = true }; 26 //將查詢對象編譯為SQL腳本 27 var sql = sqlCompiler.Compile(); 28 Console.WriteLine(sql); 29 //執行sql腳本 30 var reader = dbHelper.Read(sql, sqlCompiler.ParameterValues); 31 //驗證返回的結果中只有一條記錄,且訂單編號與參照訂單的編號相同 32 Assert.Equal(reader.Read(), true); 33 Assert.Equal(findOrder.BillNo, reader["BillNo"]); 34 Assert.Equal(reader.Read(), false);//驗證只能執行讀取一次操作 35 }
【用途】
1.適用於DDD中聚合根的查詢
2.適用於普通的面向數據表及視圖的查詢
3.可以很方便的實現可重用的面向最終用戶自助查詢(條件組合完全由用戶來指定)
受時間所限,這部分內容如有必要后續再補充吧,寫文章確實太費時間。
我不太擅長寫作,導致大家看完博文后,可能還是很難明白此通用查詢具體能做什么,補充一個稍復雜的應用示例吧。
下面的定義的查詢對象編譯后將生成SQL腳本(為了增強閱讀性,我添加了換行和縮進):
SELECT * FROM xzcOrder AS o WHERE (o.BillNo LIKE 'PO%' OR NOT o.BillDate >= '2014-1-1 0:00:00' ) AND EXISTS(SELECT 1 FROM supplier WHERE supplier.Id=o.SupplierId AND supplier.Code LIKE '%X%') AND o.Invalid = 0 AND EXISTS(SELECT 1 FROM Items AS d WHERE d.BillId=o.BillId AND EXISTS(SELECT 1 FROM Product AS p WHERE p.Id=d.ProductId AND p.Unit = '部')) AND EXISTS(SELECT 1 FROM Items AS d WHERE d.BillId=o.BillId AND d.Qty < o.TotalQty)
相應的C#代碼
1 var helper = new TypeInfoHelper<Order>(); 2 var query = new Query(typeof(Order)) 3 { 4 RootExpression = helper.GetProperty(p => p.BillNo).StartsWith("PO") //單號以PO打頭 5 .Or(helper.GetProperty(p => p.BillDate).GreaterThanOrEqualTo(new DateTime(2014, 1, 1)).Not())//訂單日期 不 大於等於 2014-1-1 6 .Unitary() //上面兩個條件設為獨立整體,即用()號包圍 7 .And(helper.GetProperty(p => p.Supplier.Code).Contains("X"))//供應商編碼中包含字符"X" 8 .And(helper.GetProperty(p => p.Invalid).EqualTo(false)) //訂單失效標志為否 9 .And(helper.GetProperty(p => p.Items.FirstOrDefault().Product.Unit).EqualTo("部")) //訂購產品的計量單位為"部" 10 .And( 11 //訂單明細中各項訂購數量小於訂單總訂購數量,僅為測試,無實際意義 12 helper.GetProperty(p => p.Items.FirstOrDefault().Qty) 13 .LessThan(helper.GetProperty(p => p.TotalQty))) 14 }; 15 List<Order> items; 16 var sqlCompiler = new SqlWhereClauseCompiler(query) { GenSelectPart = true }; 17 var sql = sqlCompiler.Compile(); 18 Console.WriteLine(sql);
【源碼下載】
很多人感興的可能都是有沒有提供源碼下載,特別是初學者;我想說的有源碼拿在手上固然是有一種踏實的感覺,但是源碼之外的設計思路更重要。
這個通用查詢方案的源碼並沒有太多技術含量,設計思路也並不高大上,因為我一向都是個實用主義者。所以你完全有可能在了解我的思路之后,在此基 礎上提煉升華,用更好的技術來實現你自己的通用查詢方案也絕非難事,我此舉開源僅為拋磚引玉,聽取大家意見,博文開頭也已經作出說明了。
源碼我就不搞什么回貼發郵件了,但是源碼還遠未成熟,還有一些代碼重構工作要做,另外也希望在得到園子里大牛們指點進一步完善代碼后再上傳到源碼托管服務器上,所以這里暫時就用百度雲盤分享給大家吧。
鏈接:
http://pan.baidu.com/s/1bn7y7oF 密碼:p2dg
最近更新下載
鏈接:
http://pan.baidu.com/s/1o64u8UQ 密碼:qo6i
代碼已托管到開源中國,地址:
http://git.oschina.net/xant77/Xant.Querier
【關於編譯項目】
開發環境:Visual Studio 2013
第三方庫:xunit、SQLite
將源碼解壓到磁盤上,打開解決方案,在"解決方案資源管理器"中顯示有兩個第三方庫存在沖突

試過啟用NuGet程序包還原,還原后編譯能通過,但是執行時會報告缺少SQLite.Interop.dll文件,不知道有沒有更好的辦法, 一個可行的解決辦法是:在Tester項目上點右鍵,選擇"管理NuGet程序包...",將已安裝的2個包刪除,然后再聯機搜索這兩個包安裝上去。
注意搜索sqlite時會出現很多相關的包,請選擇下圖這個

之后便可以編譯通過了,Tester項目可以編譯后直接運行或是在"測試資源管理器"中點擊"全部運行"查看測試方法執行結果。

如果看不到上圖的畫面,請先安裝
xUnit.net runner for Visual Studio 2012 and 2013
建議你先閱讀XunitTest.cs文件中的代碼,可以對整個項目有一個直觀了解。
【寫在最后】
博文除了圖片以外絕大部分文字內容都是我在上下班乘坐地鐵或公交時在手機上完成的,寫作不昜,如果對你還有點用處或啟發的話,望不吝點
贊,謝謝!