Linq to Sql:N层应用中的查询(下) : 根据条件进行动态查询

时间:2022-01-23 19:22:34

原文:Linq to Sql:N层应用中的查询(下) : 根据条件进行动态查询

如果允许在UI层直接访问Linq to Sql的DataContext,可以省去很多问题,譬如在处理多表join的时候,我们使用var来定义L2S查询,让编译器自动推断变量的具体类型(IQueryable<匿名类型>),并提供友好的智能提示;而且可以充分应用L2S的延迟加载特性,来进行动态查询。但如果我们希望将业务逻辑放在一个独立的层中(譬如封装在远程的WCF应用中),又希望在逻辑层应用Linq to sql,则情况就比较复杂了;由于我们只能使用var(IQueryable<匿名类型>),而var只能定义方法(Method)范围中声明的变量,出了方法(Method)之后IDE就不认得它了;在这种对IQueryable<匿名类型>一无所知的情况下,又希望能在开发时也能应用上IDE的智能感应,我们该怎么定义层之间交互的数据传输载体呢?又如何对它进行动态查询呢?

内容比较多,分上下两篇,上篇了介绍查询返回自定义实体,本篇介绍动态查询。

在我们的日常开发中,时常需要根据用户在UI上的输入来进行动态查询。在Ado.Net主宰的旧石器时代,一般会这样来动态拼接SQL查询条件:

   1: string filter = " 1=1";
   2: if(XXOO文本框不为空)
   3:     filter += string.Format(" AND XXOO='{0}', XXOO)";
   4: if(OOXX文本框不为空)
   5:     filter += string.Format(" AND OOXX='{0}', OOXX)";
   6: gridView.DataSource = BusinessLogic.XOXOQuery(filter);

然后将过滤条件传给业务逻辑层,由业务逻辑层拼接出完整的TSQL语句。  但到了LINQ to SQL时代,我们该办了呢?还要继续玩字符串拼接游戏吗?

后面将以NorthWind为例,动态查询产品(Product)及其供应商信息(Supplier):Linq to Sql:N层应用中的查询(下) : 根据条件进行动态查询

   1: partial class ProductExt : Products
   2: {
   3:     public string CompanyName { get; set; }
   4: }
1. UI层直接访问DataContext

如果使用L2S查询延迟加载的特性,动态查询也变得相当简单:

   1: public void TestDynamicQuery()
   2: {
   3:     using (NorthWindDataContext context = new NorthWindDataContext())
   4:     {
   5:         var query = from P in context.Products
   6:                            join S in context.Suppliers
   7:                             on P.SupplierID equals S.SupplierID
   8:                            select new
   9:                            {
  10:                                P.ProductName,
  11:                                P.UnitPrice,
  12:                                P.QuantityPerUnit,
  13:                                S.CompanyName
  14:                            };
  15:         if(XXOO)
  16:             query = query.Where(p => p.ProductName.Contains("che"));
  17:         if(OOXX)
  18:             query = query.Where(p => p.UnitPrice >= 20);
  19:         gridView.DataSource = query.ToList(); //延迟加载,ToList时才进行运算
  20:         gridView.DataBind();
  21:     }
  22: }

看起来还是比较舒服的,不用再继续拼接SQL了,开发时也可以充分利用IDE的智能感应。

但也不是无可挑剔,这里的逻辑无法复用。假如另外一个应用场景,要根据供应商名称来查询产品信息,我们该怎么处理呢,另外再写一个查询?如果再多一个引用场景呢,难道我们每次都要Ctrl+C | Ctrl +V?还是把这个逻辑封装在业务逻辑层,让多个的页面都可以使用?

2. 分层后引发的问题

分层的好处之一就是逻辑复用。在Ado.Net时代,我们可以把这个join操作放在业务逻辑层,UI层只需要根据不同的应用场景,拼接where条件,然后传给业务逻辑层处理即可。

当在分层应用中使用L2S时,如果想把这个逻辑放到业务逻辑层,我们或许可以这样做:

2.1.  继续拼接

    或许我们想过继续按照旧石器时代的做法,直接拼接;但是我们立刻会发现显然是行不通的,我们无法“直接”将L2S查询与字符串进行拼接

2.2. 构造Expression或者Func

query.Where()可以接受一个表达式Expression<Func<TSource, bool> predicate>或者委托Func<TSource, bool> predicate,或许我们想过尝试构造这样的Expression或者Func;但是我们又会遇到新的问题,如上面的查询,我们的query的类型是IQueryable<匿名类型>,匿名类型的定义是在编译阶段才由编译器创建的,开发时我们根本不知道TSource是类型,又该怎么创建这样的Expression或者Func呢

3. 使用Dynamic LINQ继续拼接游戏

上面2.1中提到无法“直接”将L2S查询与字符串进行拼接,但是可以通过一些扩展来间接达到目的,网上已经有人这么做了,具体可以参考:Dynamic LINQ。下面是一个示例:

   1: Northwind db = new Northwind(connString); 
   2: var query =
   3:     db.Customers.Where("City == @0 and Orders.Count >= @1", "London", 10).
   4:     OrderBy("CompanyName").
   5:     Select("New(CompanyName as Name, Phone)");

看起来,貌似我们又可以继续玩字符串拼接了。不过需要注意的一点儿是,这里拼接的字符串不再是TSQL中的字符串命令了,而是L2S查询。这是基于如下原因:在L2S中,查询被表示为一个表达式目录树(Expression Tree,表示的是数据,不是代码),待需要访问查询结果集时(针对延迟加载的情况),这棵树才被对应的Provider(这里用的是SQL Server,所以对应的是SqlProvider)翻译为TSQL,并发送给ADO.Net来执行;Dynamic LINQ就是将传进来的字符串解析为表达式目录树,并与原来的L2S进行适当地合并,从而得到最终的表达式目录树。

根据字符串进行拼接,是一种解决办法。但是这样做有个不好的地方,就是我们失去了IDE的智能感应。

4. 对IQueryable进行动态查询扩展

上面2.2节中,还提到了另外一种处理思路,那就是构造Expression或者Func;当然,这里会遇到上面提到的问题:我们的query的类型是IQueryable<匿名类型>,开发时根本不知道其具体类型,如何创建Expression<Func<匿名类型, bool> predicate>或者委托Func<匿名类型, bool> predicate呢

下面是我实现过程中的那艰苦卓越的辛酸历程:

还是拿上面的查询作为例子,譬如要查询ProductName.Contains("che")) && UnitPrice >= 20的记录;则我们能构造出来的需要构造出来的表达式会是什么样子呢?下面是两者之间的差距:

Expression<Func<Products, bool>> predicate = t => t.ProductName.Contains("che") && t.UnitPrice >= 22 //Can Do
Expression<Func<匿名类型, bool>> predicate = t => t.ProductName.Contains("che") && t.UnitPrice >= 22 //To DO

差距呢?乍一看,这就是一对双胞胎啊,还需要转换个啥子,吃饱撑的啊……
    不过细看之后,二者确有不同之处,下面是补全后的对比:

Expression<Func<Products, bool>> predicate = (Products  t) => t.ProductName.Contains("che") && t.UnitPrice >= 22
Expression<Func<匿名类型, bool>> predicate = (匿名类型 t) => t.ProductName.Contains("che") && t.UnitPrice >= 22

现在可以看到,这不是一对普通的双胞胎,基因中的软色体都不是一个样子,这是一对龙凤胎。由于.Net是强类型语言,IEnumerable<TSource>.Where()方法只认得后者,而拒绝接受前者,因此接下来,我们的目标是……没有蛀牙?NO,基因手术……当然,也希望手术的副作用包括没有蛀牙(bug)。

------------------我是华丽的分割线(happyhippy.cnblogs.com)--------------------

上一篇中,我实现了一个对象转换器,可以把一个对象转换成另一个对象;但这里用不上,这里需要换的是基因,需要把一种类型换成另一种类型。所以需要急切实现的一个函数就是,能把一个LambdaExpression的参数类型换成另一种类型,于是我实现了下面的方法:(其中,TSource为源类型,TResult为目标类型)

public static Expression<Func<TResult, bool>> Replace<TSource, TResult>( 
    this Expression<Func<TSource, bool>> predicate)
{
    ParameterConverter pc = new ParameterConverter(predicate);
    return (Expression<Func<TResult, bool>>)pc.Replace(typeof(TResult));
}

在开始写这段代码之前,我的表达式目录树知识几乎为0;于是又开始翻MSDN,找到了这里:LINQ 中的表达式目录树……最终在MSDN的帮助下,我终于把它给实现出来了,完成后我不禁沾沾自喜(虽然只有几十行代码,可行代码不到十来行,剩下的是从MSDN中的ExpressionVisitor盗版的,但也耗了我整整一个半天)……

古人云:乐极生悲。看来这句话还是有道理的。庆幸之后,接着我又坠入了万丈深渊,因为我不知道怎么调用这个方法!在这个方法外部,我们的query是IQueryable<匿名类型>,在IQueryable<匿名类型>.Where()方法中,尝试调用这个Replace方法的时候,我不知道该传什么类型参数给TResult。我又白干了……

有时候,看起来只有一步之遥,但其实天各一方……

------------------我是可耻的分割线(happyhippy.cnblogs.com)--------------------

有时候,看起来貌似遥不可及,但其实近在咫尺……

前面提到:在L2S中,查询被表示为一个表达式目录树(Expression Tree,表示的是数据,不是代码)。我既然可以向上面这样修改Expression<Func<TSource, bool>>,那我应该也可以修改这个这个LINQ查询,而且Dynamic LINQ也正是在修改LINQ查询啊。

看了下Dynamic LINQ中对Where的扩展,才知道IQueryable公布了其Provider属性:IQueryable.Provider,我们可可以直接调用Provider.CreateQuery来对原有的query进行扩充:

Expression<Func<Products, bool>> predicate = t => t.ProductName.Contains("che") && t.UnitPrice >= 22;
IQueryable query = from P in context.Products
                   join S in context.Suppliers
                   on P.SupplierID equals S.SupplierID
                   select new
                   {
                       P.ProductName,
                       P.UnitPrice,
                       P.QuantityPerUnit,
                       S.CompanyName
                   };
query = query.Provider.CreateQuery(
    Expression.Call(
        typeof(Queryable), "Where",
        new Type[] { query.ElementType }, 
        query.Expression, predicate.Replace<Products>(query.ElementType)));

前面,我无法完成将TSource(Products)强制转换成匿名类型;但这里,通过构造Expression,来将类型弱化,最终将通过Expression的编译和执行功能,来实现这种转换。

当然,每次这样写的话,我也觉得麻烦;于是,就有了下面的对IQueryable的扩展:

public static IQueryable DynamicWhere<T>(this IQueryable query, Expression<Func<T, bool>> predicate)
{
    if (predicate == null)
        return query;
 
    return query.Provider.CreateQuery(
        Expression.Call(
            typeof(Queryable), "Where",
            new Type[] { query.ElementType },
            query.Expression, predicate.Replace<T>(query.ElementType)));
}
 
//然后就可以这样用了:
public List<ProductExt> TestDynamicQuery3()
{
    using (NorthWindDataContext context = new NorthWindDataContext())
    {
        IQueryable query = from P in context.Products
                           join S in context.Suppliers
                            on P.SupplierID equals S.SupplierID
                           select new
                           {
                               P.ProductName,
                               P.UnitPrice,
                               P.QuantityPerUnit,
                               S.CompanyName,
                               S.Address
                           };
        query = query.DynamicWhere((Products p) => p.ProductName.Contains("che"))
            .DynamicWhere((Suppliers s) => s.Address == "P.O. Box 78934") 
            //.DynamicWhere((ProductExt p) => p.CompanyName == p.ProductName) //BinaryExpression右边不能有对参数的引用
            .DynamicWhere((Products p) => p.UnitPrice >= 5 * 4 + 2);
 
        return query.ConvertTo<ProductExt>();
    }
}

由于Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)已经被可耻地占去了,所以这里我定义了一个自己的方法名:DynamicWhere。

------------------又可耻了一次的分割线(happyhippy.cnblogs.com)--------------------

最后,来说说这种方法的不足之处:
    (1). 由于我们在Expression<Func<TSource, bool>> predicate时,使用的源类型TSource与query中元素类型(匿名类型)之间的属性集可能存在不同,因此这里的Expression中,只能使用匿名类型中已经声明的属性,使用不属于该匿名类型的属性时,编译时不会抱错,但运行时会出错。例如,我还补充传入了一个根据供应商所在城市的过滤条件:.DynamicWhere((Suppliers s) => s.City== "London"),运行时就挂了……这就又遇到上一节中同样的问题:UI层怎么知道属性可用,哪些属性被阉割了呢?这又是一个问题,暂时只能说:源代码前没有秘密。
    (2). BinaryExpression中的右侧表达式不能包括对参数的应用。譬如上面代码中注释掉的一行,引用了参数p,执行会报错;这是因为在处理类型参数转换时,我对BinaryExpressio中的右侧表达式CallExpression中的参数表达式进行了运算,转得到常量表达式。应该还有更好的思路,判断这些Expresion是否引用了参数,如果引用了参数,则不进行运算,如果没有引用参数,则进行运算。但是我还没有考虑出来该怎么来判断……于是就成了这个样子。不过对于动态查询来说,一般情况下应该够用了,以后想到更好的思路再加进去。

5. 如何进行逻辑复用

为了将思路描述清楚,前面我只介绍了如何进行动态查询,而刻意避开了一个问题,就是如何进行逻辑复用。问题要分解开来,然后再逐个击破~

5.1 UI与业务逻辑层位于同一地址空间(同一个应用程序域)

既然位于同一地址空间,那就可以在UI层创建Expression<Func<TSource, bool>> predicate,然后传入业务逻辑层:

public List<ProductExt> TestDynamicQuery(Expression<Func<ProductExt, bool>> predicate)
{
    using (NorthWindDataContext context = new NorthWindDataContext())
    {
        IQueryable query = from P in context.Products
                           join S in context.Suppliers
                            on P.SupplierID equals S.SupplierID
                           select new
                           {
                               P.ProductName,
                               P.UnitPrice,
                               P.QuantityPerUnit,
                               S.CompanyName
                           };
        return query.DynamicWhere(predicate).ConvertTo<ProductExt>();
    }
}
//不同场景下的应用:
//场景1
Expression<Func<ProductExt, bool>> predicate = t => t.ProductName.Contains("che") && t.UnitPrice >= 22;
return TestDynamicQuery2(predicate);
//场景2
Expression<Func<ProductExt, bool>> predicate = t => t.CompanyName == "New Orleans Cajun Delights";
return TestDynamicQuery2(predicate);

     但是这样又引入了新的问题,如何根据用户的输入条件,动态构造这个呢? t => t.ProductName.Contains("che") && t.UnitPrice >= 22;

虽然可以像下面5.2一样来处理,但是也还是有点儿麻烦;理想情况下,我希望可以像下面这样来构造predicate,这样,我们就可以使用&、| 、&=、|=来任意拼接过滤条件了:

   1: Expression<Func<ProductExt, bool>> predicate = null;
   2: predicate &= (t => t.ProductName.Contains("che")) | (t => t.UnitPrice >= 22);

5.2 UI与业务逻辑层位于不同地址空间(跨应用程序域)

如果UI与业务逻辑位于不同的地址空间,Expression<Func<TSource, bool>> predicate就没有办法跨进程传递。

一个可选办法是,将各个查询条件值作为参数(如果参数较多的话,或者经常变化的话,可以引入参数对象,具体可参考《重构》),传到业务逻辑然后再构造Expression。如果您有好的思路,欢迎一起交流。

   1: NorthWindDataContext context = new NorthWindDataContext();
   2:  
   3: public List<ProductExt> TestDynamicQuery(string productName, decimal? unitPrice)
   4: {
   5:     IQueryable query = TestDynamicQueryAll(); //开放了IQueryable,延迟加载
   6:     if (!string.IsNullOrEmpty(productName))
   7:         query = query.DynamicWhere((ProductExt p) => p.ProductName.Contains(productName));
   8:     if (unitPrice.HasValue)
   9:         query = query.DynamicWhere<ProductExt>(p => p.UnitPrice >= unitPrice);
  10:  
  11:     return query.ConvertTo<ProductExt>();
  12: }
  13:  
  14: protected IQueryable TestDynamicQueryAll()
  15: {
  16:     IQueryable query = from P in context.Products
  17:                        join S in context.Suppliers
  18:                         on P.SupplierID equals S.SupplierID
  19:                        select new   //匿名类型
  20:                        {
  21:                            P.ProductName,
  22:                            P.UnitPrice,
  23:                            P.QuantityPerUnit,
  24:                            S.CompanyName
  25:                        };
  26:     return query;  //延迟加载
  27: }

如果允许IQueryable满天飞的话,就没有5.1中提到的动态构造Expression的麻烦问题了。但是貌似看起来还是有点儿烦,能不能了个继续偷懒呢?

6. 上代码

对IQueryable的DynamicWhere扩展,及对Expression<Func<TSource, bool>>的Replace扩展:Linq2SqlExtension.rar