原文: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层应用中的查询(下) : 根据条件进行动态查询