Linq to Entity经验 - 动态查询
[摘要]本文介绍Linq to Entity如何处理动态条件查询的问题,并提供详细的示例代码供参考。
上篇文章(Linq to Entity经验 - 表达式转换)我分享了在使用Ling to Entity时,遇到的一个表达式转换问题,其主要解决的是让UI层调用数据查询时能够实现最大程度上的封装,使得我们的业务逻辑层在处理数据查询时更为精简,不再需要每一个条件写段逻辑。这篇我来总结下我们项目中是如何处理动态条件查询的问题。
问题:
如何解决动态条件查询,而继续保证业务逻辑层的稳定性?
场景:
搜索学生信息,我们可能按学号搜索,也可能按姓名搜索,还有可能按班级搜索,当然也有可能是其它条件,最复杂的情况是同时按多个条件查询。
传统解决方案:
遇到这种情况,基本上有两种类似的方法:
1、拼接动态SQL
因为不知道查询条件,所以可以采用拼接SQL字符串的形式来完成,它的缺点如下:
缺点一:需要注意SQL注入,尽管我们可以采用参数化来解决。
缺点二:需要人工去做这件事情。
缺点三:这样的需求多了,也会大大增加程序员的工作量。
2、大的通用性存储过程
在存储过程中定义多个参数,然后在存储过程内部判断使用哪些条件,这个方案也有缺点:
缺点一:程序中需要写大量这种有动态条件查询需求的存储过程,且逻辑相对复杂。
缺点二:存储过程有自身的一些缺点,这里就不多讲了。
解决方案:
充分利用表达式树的作用来动态构建查询条件,以保证业务逻辑层的稳定性,减轻工作量。
下面是我们业务逻辑层的查询接口方法:
1 | 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,如果有需要还可以扩展其方法。
1 | Expression<Func<AllocationPlan, bool>> predicate = p => p.IsActive; |
2 | if (planCondition.Project != 0) { predicate = predicate.And(c => c.ProjectId == planCondition.Project); } |
3 | if (planCondition.PlanType != 0) { predicate = predicate.And(c => c.AllocationTypeId.Value == planCondition.PlanType); } |
方案原理:
将两个表达式合并在一起,其实无论如何组织条件,超不出两类常见的表达式:
- And,对应SQL中的=,比如Where Name="Tom",它可以将多个条件And在一起变成 Where Name="Tome" and ClassId=1
- Or,对应SQL中的or,比如 Where EmployeeId=0 or EmployeeId=2
这里不讨论SQL中的一些高级函数用法,只解决常见问题,下面是一老外写的,能够很好的解决动态条件查询时的表达式创建问题,可供参考:
1 | public static class PredicateBuilderUtility |
2 | { |
3 | public static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge) |
4 | { |
5 | // build parameter map (from parameters of second to parameters of first) |
6 | var map = first.Parameters.Select((f, i) => new { f, s = second.Parameters[i] }).ToDictionary(p => p.s, p => p.f); |
7 | // replace parameters in the second lambda expression with parameters from the first |
8 | var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body); |
9 | // apply composition of lambda expression bodies to parameters from the first expression |
10 | return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters); |
11 | } |
12 | public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) |
13 | { |
14 | return first.Compose(second, Expression.AndAlso); |
15 | } |
16 | public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) |
17 | { |
18 | return first.Compose(second, Expression.Or); |
19 | } |
20 | } |
这里有一个需要特别注意,就是多个表达式中的参数问题,有的时候将多个表达式合并在一起后,虽然程序中看起来没有什么问题,但当EntityFramwork执行数据库查询时会提示:参数p没有绑定之类的异常信息,它的目的就是统一多个表达式中的参数p。
比如:
表达式1:Expression<Func<AllocationPlan, bool>> predicate = p => p.IsActive;
表达式2:Expression<Func<AllocationPlan, bool>> predicate2 = p => p.Id>0;
某些情况下我们需要将上面两个表达式合并成一个,然后调用数据库查询,处理不当就会出现上面的错误。
1 | public class ParameterRebinder : ExpressionVisitor |
2 | { |
3 | private readonly Dictionary<ParameterExpression, ParameterExpression> map; |
4 | public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map) |
5 | { |
6 | this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>(); |
7 | } |
8 | public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp) |
9 | { |
10 | return new ParameterRebinder(map).Visit(exp); |
11 | } |
12 | protected override Expression VisitParameter(ParameterExpression p) |
13 | { |
14 | ParameterExpression replacement; |
15 | if (map.TryGetValue(p, out replacement)) |
16 | { |
17 | p = replacement; |
18 | } |
19 | return base.VisitParameter(p); |
20 | } |
21 | } |
总结:
有了表达式合并的工具类,再结合仓储接口,我们可以写出简单容易理解动态条件查询的程序,也解决了其它传统方案的一些缺点,但这种方案自身也可能有自身的适用场景,适用自身项目的就是最优的,这是我的座右铭。