Linq to Entity经验:动态查询

    上篇文章(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);
    }
}

    

     总结:
     有了表达式合并的工具类,再结合仓储接口,我们可以写出简单容易理解动态条件查询的程序,也解决了其它传统方案的一些缺点,但这种方案自身也可能有自身的适用场景,适用自身项目的就是最优的,这是我的座右铭。

posted on 2012-10-28 13:04  min.jiang  阅读(9934)  评论(5编辑  收藏  举报