上篇文章(Linq to Entity經驗:表達式轉換)我分享了在使用Ling to Entity時,遇到的一個表達式轉換問題,其主要解決的是讓UI層調用數據查詢時能夠實現最大程度上的封裝,使得我們的業務邏輯層在處理數據查詢時更為精簡,不再需要每一個條件寫段邏輯。這篇我來總結下我們項目是中如何處理動態條件查詢的問題。
問題:
如何解決動態條件查詢,而繼續保證業務邏輯層的穩定性?
場景:
搜索學生信息,我們可能按學號搜索,也可能按姓名搜索,還有可能按班級搜索,當然也有可能是其它條件,最復雜的情況是同時按多個條件查詢。
傳統解決方案:
遇到這種情況,基本上有兩種類似的方法:
1:拼接動態SQL
因為不知道查詢條件,所以可以采用拼接SQL字符串的形式來完成,它的缺點如下:
缺點一:需要注意SQL注入,盡管我們可以采用參數化來解決。
缺點二:需要人工去做這件事情。
缺點三:這樣的需求多了,也會大大增加程序員的工作量。
2:大的通用性存儲過程
在存儲過程中定義多個參數,然后在存儲過程內部判斷使用哪些條件,這個方案也有缺點:
缺點一:程序中需要寫大量這種有動態條件查詢需求的存儲過程,且邏輯相對復雜。
缺點二:存儲過程有自身的一些缺點,這里就不多講了。
解決方案:
充分利用表達式樹的作用來動態構建查詢條件,以保證業務邏輯層的穩定性,減輕工作量。
下面是我們業務邏輯層的查詢接口方法:
優點:
1:無論UI上的條件是什么,只要在UI層構建好查詢表達式,業務邏輯層的查詢接口是不需要變更的。
2:避免在條件中使用字符串,之前提到的兩種方法都需要傳遞條件以確定最終的表字段信息,這是極其不高明的。
3:基於Linq式的查詢,使得程序員更加容易理解及接受。
注意:
這里定義的查詢接口,是以一張主鍵為基礎的,只要定義好相關的關聯表,無論怎樣復雜,此接口都不需要額外編寫方法。
比如有一個學生表:Student,學生表有一個外鍵列ClassId,對應的是班級表,我們可以這樣寫查詢:if(班級!="") p=>p.Class.Name=="初二2008班" 即查詢初二2008班所有學生信息。但它不能解決某些特別復雜的查詢.需要按情況來決定。
調用示例:
這里先看下最終的效果。我們可以定義And,還可以定義Or,如果有需要還可以擴展其方法。
if (planCondition.Project != 0) { predicate = predicate.And(c => c.ProjectId == planCondition.Project); }
if (planCondition.PlanType != 0) { predicate = predicate.And(c => c.AllocationTypeId.Value == planCondition.PlanType); }
方案原理:
將兩個表達式合並在一起,其實無論如何組織條件,超不出兩類常見的表達式:
1:And,對應SQL中的=,比如Where Name="Tom",它可以將多個條件And在一起變成 Where Name="Tome" and ClassId=1
2:Or,對應SQL中的or,比如 Where EmployeeId=0 or EmployeeId=2
這里不討論SQL中的一些高級函數用法,只解決常見問題,下面是一老外寫的,能夠很好的解決動態條件查詢時的表達式創建問題,可供參考:
{
public static Expression<T> Compose<T>( this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
{
// build parameter map (from parameters of second to parameters of first)
var map = first.Parameters.Select((f, i) => new { f, s = second.Parameters[i] }).ToDictionary(p => p.s, p => p.f);
// replace parameters in the second lambda expression with parameters from the first
var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
// apply composition of lambda expression bodies to parameters from the first expression
return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
}
public static Expression<Func<T, bool>> And<T>( this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.Compose(second, Expression.AndAlso);
}
public static Expression<Func<T, bool>> Or<T>( this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.Compose(second, Expression.Or);
}
}
這里有一個需要特別注意,就是多個表達式中的參數問題,有的時候將多個表達式合並在一起后,雖然程序中看起來沒有什么問題,但當EntityFramwork執行數據庫查詢時會提示:參數p沒有綁定之類的異常信息,它的目的就是統一多個表達式中的參數p。
比如:
表達式1:Expression<Func<AllocationPlan, bool>> predicate = p => p.IsActive;
表達式1:Expression<Func<AllocationPlan, bool>> predicate2 = p => p.Id>0;
某些情況下我們需要將上面兩個表達式合並成一個,然后調用數據庫查詢,處理不當就會出現上面的錯誤。
{
private readonly Dictionary<ParameterExpression, ParameterExpression> map;
public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression p)
{
ParameterExpression replacement;
if (map.TryGetValue(p, out replacement))
{
p = replacement;
}
return base.VisitParameter(p);
}
}
總結:
有了表達式合並的工具類,再結合倉儲接口,我們可以寫出簡單容易理解動態條件查詢的程序,也解決了其它傳統方案的一些缺點,但這種方案自身也可能有自身的適用場景,適用自身項目的就是最優的,這是我的座右銘。