上篇文章(Linq to Entity经验:表达式转换)我分享了在使用Ling to Entity时,遇到的一个表达式转换问题,其主要解决的是让UI层调用数据查询时能够实现最大程度上的封装,使得我们的业务逻辑层在处理数据查询时更为精简,不再需要每一个条件写段逻辑。这篇我来总结下我们项目是中如何处理动态条件查询的问题。
问题:
如何解决动态条件查询,而继续保证业务逻辑层的稳定性?
场景:
搜索学生信息,我们可能按学号搜索,也可能按姓名搜索,还有可能按班级搜索,当然也有可能是其它条件,最复杂的情况是同时按多个条件查询。
传统解决方案:
遇到这种情况,基本上有两种类似的方法:
1:拼接动态SQL
因为不知道查询条件,所以可以采用拼接SQL字符串的形式来完成,它的缺点如下:
缺点一:需要注意SQL注入,尽管我们可以采用参数化来解决。
缺点二:需要人工去做这件事情。
缺点三:这样的需求多了,也会大大增加程序员的工作量。
2:大的通用性存储过程
在存储过程中定义多个参数,然后在存储过程内部判断使用哪些条件,这个方案也有缺点:
缺点一:程序中需要写大量这种有动态条件查询需求的存储过程,且逻辑相对复杂。
缺点二:存储过程有自身的一些缺点,这里就不多讲了。
解决方案:
充分利用表达式树的作用来动态构建查询条件,以保证业务逻辑层的稳定性,减轻工作量。
下面是我们业务逻辑层的查询接口方法:
IList<ObjectModel.ActionInfo> QueryByPage<TKey>(Expression<Func<ObjectModel.ActionInfo,
bool>> filter, Expression<Func<ObjectModel.ActionInfo, TKey>> orderBy,
int orderType,
int pageSize,
int pageIndex,
out
int recordsCount)
优点:
1:无论UI上的条件是什么,只要在UI层构建好查询表达式,业务逻辑层的查询接口是不需要变更的。
2:避免在条件中使用字符串,之前提到的两种方法都需要传递条件以确定最终的表字段信息,这是极其不高明的。
3:基于Linq式的查询,使得程序员更加容易理解及接受。
注意:
这里定义的查询接口,是以一张主键为基础的,只要定义好相关的关联表,无论怎样复杂,此接口都不需要额外编写方法。
比如有一个学生表:Student,学生表有一个外键列ClassId,对应的是班级表,我们可以这样写查询:if(班级!="") p=>p.Class.Name=="初二2008班" 即查询初二2008班所有学生信息。但它不能解决某些特别复杂的查询.需要按情况来决定。
调用示例:
这里先看下最终的效果。我们可以定义And,还可以定义Or,如果有需要还可以扩展其方法。
Expression<Func<AllocationPlan,
bool>> predicate = p => p.IsActive;
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
class PredicateBuilderUtility
{
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;
某些情况下我们需要将上面两个表达式合并成一个,然后调用数据库查询,处理不当就会出现上面的错误。
public
class ParameterRebinder : ExpressionVisitor
{
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);
}
}
总结:
有了表达式合并的工具类,再结合仓储接口,我们可以写出简单容易理解动态条件查询的程序,也解决了其它传统方案的一些缺点,但这种方案自身也可能有自身的适用场景,适用自身项目的就是最优的,这是我的座右铭。