打造優雅的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() 返回的是IQueryBuilder 接口,而IQueryBuilder 接口只有一個 Expression 屬性:
///
/// 動態查詢條件創建者
///
///
public interface IQueryBuilder
{
Expression<Funcbool>> Expression { get; set; }
}
于是 Like, Between, Equals, In 就可以根據這個Expression 來無限擴展了。
以下是實現Like的擴展方法:
///
/// 建立 Like ( 模糊 ) 查詢條件
///
/// 實體
/// 動態查詢條件創建者
/// 屬性
/// 查詢值
///
public static IQueryBuilder Like(this IQueryBuilder q, Expression<Funcstring>> 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<Funcbool>> lambda = Expression.Lambda<Funcbool>>(methodExp, parameter);
q.Expression = q.Expression.And(lambda);
}
return q;
}
每個方法都是對Expression進行修改,然后返回修改后的Expression,以此實現鏈式編程。
稍微有點意思的就是 In 的擴展方法(這個害我費了不少時間,前前后后可能4個小時):
///
/// 建立 In 查詢條件
///
/// 實體
/// 動態查詢條件創建者
/// 屬性
/// 查詢值
///
public static IQueryBuilder In(this IQueryBuilder q, Expression<Func> 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類型
if (IsNullableType(type))
{
type = GetNonNullableType(type);
nonNullProperty = Expression.Convert(property.Body, type);
}
Expression<Funcbool>> InExpression = (list, el) => list.Contains(el);
var methodExp = InExpression;
var invoke = Expression.Invoke(methodExp, constant, property.Body);
Expression<Funcbool>> lambda = Expression.Lambda<Funcbool>>(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基本上全由我來寫框架。盡管平時晚上也有兩個多小時可以學習和做自己的框架,但總比不上在公司能夠用上八小時來勁。一個Project下來了,又是時候總結一下,希望有空能夠繼續與大家分享。