使用 Entity Framework 大都會搭配 Lambda Expression 到資料庫去 Query 資料出來,但是通常我們只能在程式碼中先寫好查詢的條件,當遇到不同條件的時候就要再寫一組查詢的條件,那我們能不能寫一個查詢條件的產生器,可以依照我丟進去的條件參數,幫我產生不同組合的查詢條件?
當然是可以的,這時候我們就必須動用到 Expression 這個黑魔法,我們熟知的 Lambda Expression 也繼承自 Expression 這個類別,個人覺得 Expression 是個蠻神奇的東西,從 .Net Framework 3.5 就有了,它稍微顛覆了我們寫程式的方式。
Expression 是什麼?能吃嗎?
舉個例子來說明比較容易懂,假設我要做一個可以傳入 2 個 int 數值,並回傳相加結果的 function,通常這樣寫就好了。
public int Add(int a, int b)
{
return a + b;
}
不過,這個兩數相加的方法還能像下面這樣寫,而這種寫法我想各位應該不陌生,我們用 Lambda Expression 陳述一個 Func<int, int, int>
,內容是可以傳入兩個 int 參數,回傳值是 int 的方法。
Func<int, int, int> 本身已經是個方法了,裡面已經看不到陳述句的內容,但是我們可以用 Expression<TDelegate>
來讓陳述句現身。
我們可以看到陳述句的本體是 (a + b)
,參數為 a
和 b
,此外還有一些關於這個陳述句的資訊。
這時候我們來施一點魔法,我要在 Runtime 的時候把原本回傳 (a + b) 的結果變成回傳 (a + b) + b
的結果,讓原本 (1 + 2) = 3 變成 (1 + 2) + 2 = 5。
是不是從沒想過我原本寫好的方法,會在 Runtime 的時候被改變運算的邏輯?
我們來看一下它做了什麼事情,原本陳述句的本體是 (a + b),在執行 Expression.Add()
之後變成了 (a + b) + b。
Expression.Add() 方法的左邊參數傳入原本陳述句的本體,右邊參數則傳入原本陳述句的第二個 Parameter,Expression.Add() 方法就會把左右兩邊用加號連接起來,我們移至 Expression 的定義就會發現,其實 Expression 還有一堆靜態方法,都是與運算相關的陳述語句,Expression.Add() 只是其中一個。
從上面的例子可以得知 Expression 是可以讓我們在 Runtime 的時候,藉由它所提供的陳述語句,依照我們的需求來自行組合運算資料的方式。
動態條件查詢
我們回到 Entity Framework,我這邊做了一個例子,我有一個 Customer 的資料表,裡面的資料就跟下圖一樣。
我有兩個查詢需求,一個是撈出住在台北市的人,一個是撈出住在台北市大安區的人,以往的直覺跟習慣這樣寫應該是沒有什麼問題的。
程式寫好上線了,這時候需求來了,使用者要撈出住在台北市大安區姓蔡的人,這樣以後不就多一個查詢組合就再寫一個查詢方法,感覺有點不太 Smart。
我們看到 Where 擴充方法裡面要給入 Expression<Func<TSource, bool>>
參數當查詢條件,經過剛剛的例子我想應該對這個參數不陌生了吧。
既然已經知道我們可以對 Expression<Func<TSource, bool>> 動手腳了,我們就可以來開始進行改造,我們把我們的查詢條件做個設計,我另外多建立幾個類別,用來表達從使用者端丟下來的查詢條件,CustomerQueryConditions
是由 Customer 中挑選幾個屬性,開放出來提供查詢的條件。
再來我建立一個抽象類別 QueryConditionsResolver<TEntry, TConditions>
,裡面提供了 And
方法,用來組 And 條件,還定義了一個派生類別必須要實作的方法 Resolve()
,讓派生類別自己去實作查詢條件。
public abstract class QueryConditionsResolver<TEntry, TConditions>
{
private Expression predicate;
private ParameterExpression parameter;
public QueryConditionsResolver(TConditions queryConditions)
{
this.parameter = Expression.Parameter(typeof(TEntry));
this.predicate = Expression.Constant(true);
this.QueryConditions = queryConditions;
}
protected TConditions QueryConditions { get; set; }
public abstract Expression<Func<TEntry, bool>> Resolve();
protected Expression<Func<TEntry, bool>> GenerateLambdaExpression()
{
return Expression.Lambda<Func<TEntry, bool>>(this.predicate, this.parameter);
}
protected void And<TValue>(QueryCondition<TValue> queryCondition, string entryFieldName)
{
if (queryCondition != null)
{
Expression expression = null;
Expression property = Expression.Property(this.parameter, entryFieldName);
Expression constant = Expression.Constant(queryCondition.Value, typeof(TValue));
switch (queryCondition.Comparsion)
{
case QueryComparsion.GreaterThan:
expression = Expression.GreaterThan(property, constant);
break;
case QueryComparsion.LessThan:
expression = Expression.LessThan(property, constant);
break;
case QueryComparsion.Equal:
expression = Expression.Equal(property, constant);
break;
case QueryComparsion.NotEqual:
expression = Expression.NotEqual(property, constant);
break;
case QueryComparsion.LessThanOrEqual:
expression = Expression.LessThanOrEqual(property, constant);
break;
case QueryComparsion.GreaterThanOrEqual:
expression = Expression.GreaterThanOrEqual(property, constant);
break;
case QueryComparsion.StartsWith:
expression = Expression.Call(property, typeof(string).GetMethod("StartsWith", new Type[] { typeof(String) }), constant);
break;
default:
throw new NotSupportedException("不支援此類型");
}
this.predicate = Expression.And(this.predicate, expression);
}
}
}
接著我就建立一個類別 CustomerQueryConditionsResolver
繼承 QueryConditionsResolver<Customer, CustomerQueryConditions>
,並實作 Resolve() 方法,兜完查詢條件後,呼叫 GenerateLambdaExpression()
將查詢條件的 Lambda 陳述句產生出來。
public class CustomerQueryConditionsResolver : QueryConditionsResolver<Customer, CustomerQueryConditions>
{
public CustomerQueryConditionsResolver(CustomerQueryConditions customerQueryConditions)
: base(customerQueryConditions)
{
}
public override Expression<Func<Customer, bool>> Resolve()
{
this.And(this.QueryConditions.City, nameof(Customer.City));
this.And(this.QueryConditions.District, nameof(Customer.District));
this.And(this.QueryConditions.Name, nameof(Customer.Name));
return this.GenerateLambdaExpression();
}
}
準備工作完畢,這時候我們去改寫原來的查詢方法,並且把撈出住在台北市大安區姓蔡的人的需求也實作起來。
這樣子改完之後,我們的查詢條件就可以依照傳進來的 QueryConditions 去動態地組合,這樣子做我們還可以減少存取資料庫程式的變動,雖然無法徹底符合所有的查詢條件,像有些花式查詢還是沒辦法靠這樣兜出來,但是已經可以滿足大部分的需求,就算遇到需要花式查詢的時候,至少我們還有 Dapper 可以用。
參考資料
< Source Code >