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

原文: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):

   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

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

时间: 2024-09-30 00:32:26

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

在Linq to sql 和 Entity framework 中使用lambda表达式实现left join

我们知道lambda表达式在Linq to sql 和 Entity framework 中使用join函数可以实现inner join,那么怎么才能在lambda表达式中实现left join呢?秘诀就是在join后面加上一个函数DefaultIfEmpty函数,实际上这个函数在linq中貌似也只有将inner join转换为left join的作用,示例如下 var joinResult = DB.Table1s.Join(DB.Table2s, a => a.id, b => b.id,

linq中如何实现多个条件的联合查询

目前接触处理数据这一块比较多,在处理内存中的数据源的时候我一般使用的是linq,linq使用起来像sql语句一样,用法简单,功能强大. 最近需要实现一个从两个不同的文件读取不同的数据,然后根据这两个数据的一些字段经行联合,然后把他们的结果放到一个数据源里面里啊,一般的联合查询两个数据源,都是通过一个数据源的字段和另一个数据源的字段经行匹配就可以了,如果是这样的话在linq实现和在sql里面实现其实差不多,下面是单个条件的联合查询的代码如下所示 var result1 = from l1 in l

Linq to sql 实现多条件的动态查询(方法一)

/// <summary> /// Linq to sql 多字段动态查询 /// </summary> /// <returns></returns> private List<TVacant> ViewBinding(ModelDataContext db,string fyno,string brd,string area,string city,string pos) { Expression<Func<TVacant, bo

linq+lambda+delegate,从list中查找到满足匹配条件的所有数据索引值

linq的扩展方法中有FindIndex,FindLastIndex两个方法可以查找满足条件的首个和最后一个数据的索引值,利用delegate将匹配条件的方法传入FindAllIndex,查找满足匹配条件的所有索引返回 /// <summary> /// 返回list内所有满足where条件的元素的索引 /// </summary> public static List<int> FindAllIndex<T>(this List<T> list

[sql]在case语句中不同情况下then的数据的数据类型不一致ORA-00932: inconsistent datatypes: expected NUMBER got CHAR

ORA-00932: inconsistent datatypes: expected NUMBER got CHAR 00932. 00000 -  "inconsistent datatypes: expected %s got %s" 1 CASE XXXXX.FILE_TYPE 2 WHEN '0' THEN NVL(TX_CNT,0) 3 ELSE XXXXX.TOT_TX_CNT 4 END ACT_TX_CNT, TX_CNT为NUMBER型,是GROUP BY 后的计数

LINQ To SQL 语法及实例大全

LINQ to SQL语句(1)之Where Where操作 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判断条件就是它后面所接的子句. Where操作包括3种形式,分别为简单形式.关系条件形式.First()形式.下面分别用实例举例下: 1.简单形式: 例如:使用where筛选在伦敦的客户 var q = from c in db.Customers where c.City == "London" select c

c# LINQ to SQL

list例: List<result_1> list = get_lately_fifty_data(); List<int> number_count = (from c in list orderby c.Number ascending select c.Count).ToList(); datatable例: var q = from p in dt.AsEnumerable() where p.Field<string>("username"

LINQ to SQL语句(1)之Where(抄的好)

Where操作 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判断条件就是它后面所接的子句. Where操作包括3种形式,分别为简单形式.关系条件形式.First()形式.下面分别用实例举例下: 1.简单形式: 例如:使用where筛选在伦敦的客户 var q = from c in db.Customers where c.City == "London" select c; 再如:筛选1994 年或之后雇用的雇员:

LINQ to SQL攻略

LINQ to SQL语句(1)之Where 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判断条件就是它后面所接的子句.Where操作包括3种形式,分别为简单形式.关系条件形式.First()形式.下面分别用实例举例下: 1.简单形式: 例如:使用where筛选在伦敦的客户 var q = from c in db.Customers where c.City == "London" select c; 再如:筛选19