QueryBuilder : 打造优雅的Linq To SQL动态查询
首先我們來看看日常比較典型的一種查詢Form
這個場景很簡單:就是根據客戶名、訂單日期、負責人來作篩選條件,然后找出符合要求的訂單。
在那遙遠的時代,可能避免不了要寫這樣的簡單接口:
public interface IOrderService {IList<Order> Search(string customer, DateTime dateFrom, DateTime dateTo, int employeeID); }具體愛怎么實現就怎么實現啦,存儲過程,ORM框架。這里假定是用了孩童時代就開始有的存儲過程吧:
Create Procedure usp_SearchOrder @Customer nVarchar(20), @DateFrom DateTime, @DateTo DateTime, @EmployeeID Int AS /*... 以下省去幾百行SQL語句*/接著寫一個類OrderService實現IOrderService, 調用以上存儲過程,洋洋灑灑的寫上幾句代碼就可以“安枕睡覺”了。但是,噩夢就從此開始了。
客戶的需求是不斷變化的。過了一段時間,設計這個接口的工程師就先被夸贊一番,然后說客戶提出需要加多“一個”篩選條件。工程師可能也想過這一點,加一個篩選條件“不外乎“給接口加個參數,存儲過程加個參數,where那里加個條件,……苦力活一堆。
客戶的篩選條件是這樣:既然訂單里有“國家”字段,就想根據國家來篩選條件,并且國家可多選,如下圖:
工程師看到圖可能就倒下了……
以上可以當作笑話看看,不過話說回來,沒有一個通用的查詢框架,單靠這樣的接口
public interface IOrderService {IList<Order> Search(string customer, DateTime dateFrom, DateTime dateTo, int employeeID); }是根本不能適應需求變化。
在沒有Linq 的時代, SQL“ 強人”就試圖通過拼接字符串的方法來結束存儲過程帶來的痛苦,
IList<Order> Search(string sqlQurey);結果進入另一個被“SQL注入”的時代(注:我大學時也有一段時間玩過“SQL注入”,不亦樂乎,現在基本上很少找到能夠簡單注入的網站了,有磨難就有前進的動力嘛)。
來到Linq To SQL 的時代 (不得不贊嘆Linq把查詢發揮到淋漓盡致), 某些朋友就能輕易地揮灑Linq表達式來解決查詢問題:
IList<Order> Search(Expression<Func<Order, bool>> expression);查詢語句:
Expression<Func<Order, bool>> expression = c =>c.Customer.ContactName.Contains(txtCustomer.Text) &&c.OrderDate >= DateTime.Parse(txtDateFrom.Text) && c.OrderDate <= DateTime.Parse(txtDateTo.Text) &&c.EmployeeID == int.Parse(ddlEmployee.SelectedValue);然后再一次 “安枕睡覺”。一覺醒來還是不行。
客戶又來新需求:負責人的下拉框加個“ALL”的選項,如果選了“ALL”就搜索所有的負責人相關的Order。
工程師刷刷幾下,又加if else,又加 and 來拼裝expression;接著又來新需求,…… 最后expression臃腫無比 (當然這個故事是有點夸張)。
為什么用上“先進”的工具還是會倒在慘不忍睹的代碼海洋里呢?因為Microsoft提供給我們的只是“魚竿”。這種魚竿不管在小河還是大海都能釣到東西,而且不管你釣的是鯊魚還是鯨魚,也保證魚竿不會斷。但是有些人能釣到大魚,有些則釣到一雙拖鞋。因為關鍵的魚餌沒用上。也就是說,Microsoft給了我們強大的Linq 表達式,可不是叫我們隨便到表現層一放就了事,封裝才是硬道理。
于是,千呼萬喚始出來,猶抱 QueryBuilder 半遮臉:
var queryBuilder = QueryBuilder.Create<Order>().Like(c => c.Customer.ContactName, txtCustomer.Text).Between(c => c.OrderDate, DateTime.Parse(txtDateFrom.Text), DateTime.Parse(txtDateTo.Text)).Equals(c => c.EmployeeID, int.Parse(ddlEmployee.SelectedValue)).In(c => c.ShipCountry, selectedCountries );這樣代碼就清爽很多了,邏輯也特別清晰,即使不懂Linq 表達式的人也能明白這些語句是干什么的,因為它的語義基本上跟SQL一樣:
WHERE ([t1].[ContactName] LIKE '%A%') AND ? (([t0].[OrderDate]) >= '1/1/1990 12:00:00 AM') AND (([t0].[OrderDate]) <= '9/25/2009 11:59:59 PM') AND ? (([t0].[EmployeeID]) = 1) AND ? ([t0].[ShipCountry] IN ('Finland', 'USA', 'UK'))?
對于使用這個QueryBuilder的人來說,他覺得很爽,因為他明白釣什么魚用什么魚餌了,模糊查詢用Like,范圍用Between,……
對于編寫這個QueryBuilder的人來說,也覺得很爽,因為他本身熱愛寫通用型的代碼,就像博客園的老趙那樣。
?
看到使用方式,聰明人自然就已經想到大概的實現方式。就像廚師吃過別人煮的菜,自然心中也略知是怎么煮的。
實現方式并不難,這里簡單說明一下:
QueryBuilder.Create<Order>() 返回的是IQueryBuilder<T> 接口,而IQueryBuilder<T> 接口只有一個 Expression 屬性:
/// <summary> /// 動態查詢條件創建者 /// </summary> /// <typeparam name="T"></typeparam> public interface IQueryBuilder<T> {Expression<Func<T, bool>> Expression { get; set; } }于是 Like, Between, Equals, In就可以根據這個Expression 來無限擴展了。
以下是實現Like的擴展方法:
/// <summary> /// 建立 Like ( 模糊 ) 查詢條件 /// </summary> /// <typeparam name="T">實體</typeparam> /// <param name="q">動態查詢條件創建者</param> /// <param name="property">屬性</param> /// <param name="value">查詢值</param> /// <returns></returns> public static IQueryBuilder<T> Like<T>(this IQueryBuilder<T> q, Expression<Func<T, string>> property, string value) {value = value.Trim();if (!string.IsNullOrEmpty(value)){var parameter = property.GetParameters();var constant = Expression.Constant("%" + value + "%");MethodCallExpression methodExp = Expression.Call(null, typeof(SqlMethods).GetMethod("Like",new Type[] { typeof(string), typeof(string) }), property.Body, constant);Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(methodExp, parameter);q.Expression = q.Expression.And(lambda);}return q; }每個方法都是對Expression進行修改,然后返回修改后的Expression,以此實現鏈式編程。
稍微有點意思的就是 In的擴展方法(這個害我費了不少時間,前前后后可能4個小時):
/// <summary> /// 建立 In 查詢條件 /// </summary> /// <typeparam name="T">實體</typeparam> /// <param name="q">動態查詢條件創建者</param> /// <param name="property">屬性</param> /// <param name="valuse">查詢值</param> /// <returns></returns> public static IQueryBuilder<T> In<T,P>(this IQueryBuilder<T> q, Expression<Func<T, P>> property, params P[] values) {if (values != null && values.Length > 0){var parameter = property.GetParameters();var constant = Expression.Constant(values);Type type = typeof(P);Expression nonNullProperty = property.Body;//如果是Nullable<X>類型,則轉化成X類型if (IsNullableType(type)){type = GetNonNullableType(type);nonNullProperty = Expression.Convert(property.Body, type);}Expression<Func<P[], P, bool>> InExpression = (list, el) => list.Contains(el);var methodExp = InExpression;var invoke = Expression.Invoke(methodExp, constant, property.Body);Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(invoke, parameter);q.Expression = q.Expression.And(lambda);}return q; }?
如果有興趣的朋友可以在文章末下載源代碼,看看其他兩個擴展方法。
嗯,似乎又是時候退場了。什么?你說只有Like, Between, Equals, In不夠用?哦,可以自己擴展IQueryBuilder,自己動手豐衣足食嘛。
我后來又為另外一個Project做了一個“奇怪”的擴展:
譬如,我們知道打印設置里可以直接寫頁數號碼來篩選要打印哪幾頁——1,4,9 或者 1-8 這樣的方式。
于是引發這樣的需求:
左圖查詢Bruce和Jeffz的訂單;右圖查詢B直到Z的客戶訂單。
還美其名曰:Fuzzy ,意即:模糊不清的 (點這里看 Fuzzy詳細意思)
總結:
其實這篇文章已經醞釀好久了,近期工作收獲很多編程技巧。一個Project下來了,又是時候總結一下,希望有空能夠繼續與大家分享。
源代碼下載:CoolCode.Linq.rar
建議參考新一篇《QueryBuilder : 打造優雅的Linq To SQL動態查詢(支持EF、.Net4)》
參考:
http://www.cnblogs.com/neuhawk/archive/2007/07/07/809585.html
http://www.cnblogs.com/billgan/archive/2009/01/08/1371809.html
http://www.cnblogs.com/lyj/archive/2008/03/25/1122157.html
http://www.cnblogs.com/126/archive/2007/09/09/887723.html
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的QueryBuilder : 打造优雅的Linq To SQL动态查询的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 技术、管理和技术管理
- 下一篇: 陈丕宏:公司领导人对企业文化的影响