QueryBuilder : 打造优雅的Linq To SQL动态查询
首先我們來(lái)看看日常比較典型的一種查詢(xún)Form
這個(gè)場(chǎng)景很簡(jiǎn)單:就是根據(jù)客戶(hù)名、訂單日期、負(fù)責(zé)人來(lái)作篩選條件,然后找出符合要求的訂單。
在那遙遠(yuǎn)的時(shí)代,可能避免不了要寫(xiě)這樣的簡(jiǎn)單接口:
public interface IOrderService {IList<Order> Search(string customer, DateTime dateFrom, DateTime dateTo, int employeeID); }具體愛(ài)怎么實(shí)現(xiàn)就怎么實(shí)現(xiàn)啦,存儲(chǔ)過(guò)程,ORM框架。這里假定是用了孩童時(shí)代就開(kāi)始有的存儲(chǔ)過(guò)程吧:
Create Procedure usp_SearchOrder @Customer nVarchar(20), @DateFrom DateTime, @DateTo DateTime, @EmployeeID Int AS /*... 以下省去幾百行SQL語(yǔ)句*/接著寫(xiě)一個(gè)類(lèi)OrderService實(shí)現(xiàn)IOrderService, 調(diào)用以上存儲(chǔ)過(guò)程,洋洋灑灑的寫(xiě)上幾句代碼就可以“安枕睡覺(jué)”了。但是,噩夢(mèng)就從此開(kāi)始了。
客戶(hù)的需求是不斷變化的。過(guò)了一段時(shí)間,設(shè)計(jì)這個(gè)接口的工程師就先被夸贊一番,然后說(shuō)客戶(hù)提出需要加多“一個(gè)”篩選條件。工程師可能也想過(guò)這一點(diǎn),加一個(gè)篩選條件“不外乎“給接口加個(gè)參數(shù),存儲(chǔ)過(guò)程加個(gè)參數(shù),where那里加個(gè)條件,……苦力活一堆。
客戶(hù)的篩選條件是這樣:既然訂單里有“國(guó)家”字段,就想根據(jù)國(guó)家來(lái)篩選條件,并且國(guó)家可多選,如下圖:
工程師看到圖可能就倒下了……
以上可以當(dāng)作笑話(huà)看看,不過(guò)話(huà)說(shuō)回來(lái),沒(méi)有一個(gè)通用的查詢(xún)框架,單靠這樣的接口
public interface IOrderService {IList<Order> Search(string customer, DateTime dateFrom, DateTime dateTo, int employeeID); }是根本不能適應(yīng)需求變化。
在沒(méi)有Linq 的時(shí)代, SQL“ 強(qiáng)人”就試圖通過(guò)拼接字符串的方法來(lái)結(jié)束存儲(chǔ)過(guò)程帶來(lái)的痛苦,
IList<Order> Search(string sqlQurey);結(jié)果進(jìn)入另一個(gè)被“SQL注入”的時(shí)代(注:我大學(xué)時(shí)也有一段時(shí)間玩過(guò)“SQL注入”,不亦樂(lè)乎,現(xiàn)在基本上很少找到能夠簡(jiǎn)單注入的網(wǎng)站了,有磨難就有前進(jìn)的動(dòng)力嘛)。
來(lái)到Linq To SQL 的時(shí)代 (不得不贊嘆Linq把查詢(xún)發(fā)揮到淋漓盡致), 某些朋友就能輕易地?fù)]灑Linq表達(dá)式來(lái)解決查詢(xún)問(wèn)題:
IList<Order> Search(Expression<Func<Order, bool>> expression);查詢(xún)語(yǔ)句:
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);然后再一次 “安枕睡覺(jué)”。一覺(jué)醒來(lái)還是不行。
客戶(hù)又來(lái)新需求:負(fù)責(zé)人的下拉框加個(gè)“ALL”的選項(xiàng),如果選了“ALL”就搜索所有的負(fù)責(zé)人相關(guān)的Order。
工程師刷刷幾下,又加if else,又加 and 來(lái)拼裝expression;接著又來(lái)新需求,…… 最后expression臃腫無(wú)比 (當(dāng)然這個(gè)故事是有點(diǎn)夸張)。
為什么用上“先進(jìn)”的工具還是會(huì)倒在慘不忍睹的代碼海洋里呢?因?yàn)镸icrosoft提供給我們的只是“魚(yú)竿”。這種魚(yú)竿不管在小河還是大海都能釣到東西,而且不管你釣的是鯊魚(yú)還是鯨魚(yú),也保證魚(yú)竿不會(huì)斷。但是有些人能釣到大魚(yú),有些則釣到一雙拖鞋。因?yàn)殛P(guān)鍵的魚(yú)餌沒(méi)用上。也就是說(shuō),Microsoft給了我們強(qiáng)大的Linq 表達(dá)式,可不是叫我們隨便到表現(xiàn)層一放就了事,封裝才是硬道理。
于是,千呼萬(wàn)喚始出來(lái),猶抱 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 表達(dá)式的人也能明白這些語(yǔ)句是干什么的,因?yàn)樗恼Z(yǔ)義基本上跟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'))?
對(duì)于使用這個(gè)QueryBuilder的人來(lái)說(shuō),他覺(jué)得很爽,因?yàn)樗靼揍炇裁呆~(yú)用什么魚(yú)餌了,模糊查詢(xún)用Like,范圍用Between,……
對(duì)于編寫(xiě)這個(gè)QueryBuilder的人來(lái)說(shuō),也覺(jué)得很爽,因?yàn)樗旧頍釔?ài)寫(xiě)通用型的代碼,就像博客園的老趙那樣。
?
看到使用方式,聰明人自然就已經(jīng)想到大概的實(shí)現(xiàn)方式。就像廚師吃過(guò)別人煮的菜,自然心中也略知是怎么煮的。
實(shí)現(xiàn)方式并不難,這里簡(jiǎn)單說(shuō)明一下:
QueryBuilder.Create<Order>() 返回的是IQueryBuilder<T> 接口,而IQueryBuilder<T> 接口只有一個(gè) Expression 屬性:
/// <summary> /// 動(dòng)態(tài)查詢(xún)條件創(chuàng)建者 /// </summary> /// <typeparam name="T"></typeparam> public interface IQueryBuilder<T> {Expression<Func<T, bool>> Expression { get; set; } }于是 Like, Between, Equals, In就可以根據(jù)這個(gè)Expression 來(lái)無(wú)限擴(kuò)展了。
以下是實(shí)現(xiàn)Like的擴(kuò)展方法:
/// <summary> /// 建立 Like ( 模糊 ) 查詢(xún)條件 /// </summary> /// <typeparam name="T">實(shí)體</typeparam> /// <param name="q">動(dòng)態(tài)查詢(xún)條件創(chuàng)建者</param> /// <param name="property">屬性</param> /// <param name="value">查詢(xún)值</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; }每個(gè)方法都是對(duì)Expression進(jìn)行修改,然后返回修改后的Expression,以此實(shí)現(xiàn)鏈?zhǔn)骄幊獭?/p>
稍微有點(diǎn)意思的就是 In的擴(kuò)展方法(這個(gè)害我費(fèi)了不少時(shí)間,前前后后可能4個(gè)小時(shí)):
/// <summary> /// 建立 In 查詢(xún)條件 /// </summary> /// <typeparam name="T">實(shí)體</typeparam> /// <param name="q">動(dòng)態(tài)查詢(xún)條件創(chuàng)建者</param> /// <param name="property">屬性</param> /// <param name="valuse">查詢(xún)值</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>類(lèi)型,則轉(zhuǎn)化成X類(lèi)型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; }?
如果有興趣的朋友可以在文章末下載源代碼,看看其他兩個(gè)擴(kuò)展方法。
嗯,似乎又是時(shí)候退場(chǎng)了。什么?你說(shuō)只有Like, Between, Equals, In不夠用?哦,可以自己擴(kuò)展IQueryBuilder,自己動(dòng)手豐衣足食嘛。
我后來(lái)又為另外一個(gè)Project做了一個(gè)“奇怪”的擴(kuò)展:
譬如,我們知道打印設(shè)置里可以直接寫(xiě)頁(yè)數(shù)號(hào)碼來(lái)篩選要打印哪幾頁(yè)——1,4,9 或者 1-8 這樣的方式。
于是引發(fā)這樣的需求:
左圖查詢(xún)Bruce和Jeffz的訂單;右圖查詢(xún)B直到Z的客戶(hù)訂單。
還美其名曰:Fuzzy ,意即:模糊不清的 (點(diǎn)這里看 Fuzzy詳細(xì)意思)
總結(jié):
其實(shí)這篇文章已經(jīng)醞釀好久了,近期工作收獲很多編程技巧。一個(gè)Project下來(lái)了,又是時(shí)候總結(jié)一下,希望有空能夠繼續(xù)與大家分享。
源代碼下載:CoolCode.Linq.rar
建議參考新一篇《QueryBuilder : 打造優(yōu)雅的Linq To SQL動(dòng)態(tài)查詢(xún)(支持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
超強(qiáng)干貨來(lái)襲 云風(fēng)專(zhuān)訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的QueryBuilder : 打造优雅的Linq To SQL动态查询的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 技术、管理和技术管理
- 下一篇: 陈丕宏:公司领导人对企业文化的影响