4 、Interpreted Queries
LINQ提供了两种并行架构:
-
本地对象集合的本地查询
-
远程数据源的解释查询。
到目前为止,我们研究的都是本地查询的体系结构,它们操作在实现了 IEnumerable<T>接口的序列上。 本地查询解析为可枚举类中的查询操作符(默认情况下),而这些操作符又可以被解析为修饰符序列链(实际就是委托链)。下面开始学习解释查询。
解释查询是描述性的
解释查询不对 IEnumerable接口实现的序列操作了,它们对实现iquerizable <T> 接口的序列进行操作,并解析到Queryable类中的查询操作符,查询操作符发出在运行时解释的表达式树,而在本地查询中查询操作符编译为委托链。
Enumerable中的查询操作符实际上可以使用iquerizable <T>序列。困难在于生成的查询总是在客户机上本地执行——这就是可查询类中提供第二组查询操作符的原因。
There are two IQueryable<T> implementations in the .NET Framework:
-
LINQ to SQL
-
Entity Framework (EF)
It’s also possible to generate an IQueryable<T> wrapper around an ordinary enumerable collection by calling the AsQueryable method.
(翻译为汉语还有点别扭)可以通过调用 AsQueryable 方法来生成一个普通可枚举集合的iquerable <T>包装器。
我们先用 LINQ to SQL 说明 interpreted query 架构,因为 LINQ to SQL 不需要 Entity Data Model。 但是,我们编写的查询在 Entity Framework(以及许多第三方产品)上同样有效。
IQuerizable <T>是IEnumerable<T>
假设我们在SQL Server中创建一个简单的customer表,并使用以下SQL脚本用几个名称填充它:
create table Customer ( ID int not null primary key, Name varchar(30) ) insert Customer values (1, 'Tom') insert Customer values (2, 'Dick') insert Customer values (3, 'Harry') insert Customer values (4, 'Mary') insert Customer values (5, 'Jay')
有了这个表,我们可以用c#编写一个解释LINQ查询来检索客户名中包含字母“a”的客户,如下所示:
using System; using System.Linq; using System.Data.Linq; // in System.Data.Linq.dll using System.Data.Linq.Mapping; [Table] public class Customer { [Column(IsPrimaryKey=true)] public int ID; [Column] public string Name; } class Test { static void Main() { DataContext dataContext = new DataContext ("connection string"); Table<Customer> customers = dataContext.GetTable <Customer>(); IQueryable<string> query = from c in customers where c.Name.Contains ("a") orderby c.Name.Length select c.Name.ToUpper(); foreach (string name in query) Console.WriteLine (name); } }
上面的LINQ to SQL 转为 SQL会像下面这样:
SELECT UPPER([t0].[Name]) AS [value] FROM [Customer] AS [t0] WHERE [t0].[Name] LIKE @p0 ORDER BY LEN([t0].[Name]) //with the following end result: JAY MARY HARRY
How Interpreted Queries Work
1、首先,编译器将 query 语法转为 fluent语法,这和本地查询完全一样。
IQueryable<string> query = customers.Where (n => n.Name.Contains ("a")) .OrderBy (n => n.Name.Length) .Select (n => n.Name.ToUpper());
2、接下来编译器解析查询操作符方法,这里解释查询就和本地查询不一样了, 解释查询解析查询操作在 Queryable 类,而不是Enumerable 类中的操作符。
要了解原因,我们需要查看customers变量,这是构建整个查询的源。 customer类型为Table<T>,它实现了iquerizable <T> (IEnumerable<T>的一个子类型)。 这意味着编译器有一个选择在解析 where 操作符的时候 :它可以调用Enumerable中的扩展方法,也可以调用query中的扩展方法:
public static IQueryable<TSource> Where<TSource> (this IQueryable<TSource> source, Expression <Func<TSource,bool>> predicate)
编译器选择 Queryable.where 因为它的签名是更具体的匹配。
Querable.where 接收一个谓词,这个谓词被封装在一个 Expression<TDelegate> 类型中。这指导编译器转换提供的lambda 表达式为一个表达式树,而不是编译为一个委托。
一个表达式树是一个对象模型,这个模型基于在 System.Linq.Expressions 中的类型,而它可以在运行时被检查。这样 LINQ to SQL or EF 以后可以把它转换成SQL语句。
因为 Queryable.Where 返回一个 IQueryable<T>,所以Orderby 和 Select 操作符也一样。 返回结果的说明见Figure 8-9. 在这个 shaded box, 有一个表达式树描述整个查询,它可以在运行时遍历。
Execution
Interpreted queries 也遵循延时执行模式,就像本地查询一样。这意味着直到你枚举这个查询时,这个SQL 语句才会生成。还有要注意,在同一个query 上枚举两次会导致在数据库上执行两次查询。
解释查询 和本地查询不同的地方在哪呢?不同之处在于它们的执行方式。
-
当您枚举 Interpreted queries时, 最外层的序列运行一个程序,该程序贯穿整个表达式树,将其作为一个单元处理。
我们前面说 LINQ query 像是一个生产线。但是,当您枚举一个iquerable输送带时,它不会像本地查询那样启动整个生产线。相反,它只是启动了 IQueryable 传送带,并使用了一个特殊的枚举器,这个枚举器称之为 生产经理。 经理审查整个生产线,其中不包括已编译的代码。 经理检查整个生产线,它不是由编译的代码组成的,而是由一些傻瓜(方法调用表达式)组成的,并将指令粘贴到他们的前额(表达式树)。然后经理遍历所有表达式,在本例中,将它们转录到一张纸上(一条SQL语句),然后执行该语句,将结果反馈给使用者。 只有一条皮带转动,生产线的其余部分是一个由空壳组成的网络,存在只是为了描述必须要做什么。
在本地查询中你可以写一个自己的 query 方法,但是在 remote query 中就不可能了。因为枚举器不会识别你自己写的 query 方法。
可查询的标准方法集定义了用于查询任何远程集合的标准词汇表。
Combining Interpreted and Local Queries
一个查询可以既有本地查询也有解释查询。典型模式是本地查询在外部,解释查询在内部。换句话说,这个解释查询 的结果可作为 本地查询的输入。
假设我们要写一个 custom 表达式方法 来pair up 字符串在一个集合中。
public static IEnumerable<string> Pair (this IEnumerable<string> source) { string firstHalf = null; foreach (string element in source) if (firstHalf == null) firstHalf = element; else { yield return firstHalf + ", " + element; firstHalf = null; } }
上面定义了一个本地查询方法,可以在一个 解释查询中内使用这个方法。
DataContext dataContext = new DataContext ("connection string"); Table<Customer> customers = dataContext.GetTable <Customer>(); IEnumerable<string> q = customers .Select (c => c.Name.ToUpper()) .OrderBy (n => n) .Pair() // Local from this point on. .Select ((n, i) => "Pair " + i.ToString() + " = " + n); foreach (string element in q) Console.WriteLine (element); //输出结果为 Pair 0 = HARRY, MARY Pair 1 = TOM, DICK
因为 customers 是一个实现了 IQueryable<T>接口的类型,所以 Select 操作符要使用 Querable.Select ,而它又返回的是 IQueryable<T>接口的类型,所以后面的 OrderBy 也是 Querable.OrderBy。但是接下来的操作符 Pair 是自己写的,它实现的是 IEnumerable<T>, 因此,它解析为我们的本地Pair 方法——将interpreted query 包装在本地查询中。Pair还返回IEnumerable,因此后面的Select 也会解析为另一个本地操作符。
剩下的工作在本地完成。实际上,我们最终得到一个本地查询(外部),它的源是一个解释查询(内部)。
AsEnumerable
Enumerable.AsEnumerable 是所有 query 操作符中最简单的一个。下面是它的完整定义。
public static IEnumerable<TSource> AsEnumerable<TSource>(this IEnumerable<TSource> source) { return source; }
它的目的是将IQuerable <T>序列转换为IEnumerable<T>, 强制后续查询操作符绑定到Enumerable操作符,而不是Querable 操作符。这将导致查询的其余部分在本地执行。
假设我们有一个 MedicalArticles 表在 SQL Server 中,想用 LINQ to SQL or EF 来检索所有摘要少于100字的流感文章, 对于后一个谓词,我们需要一个正则表达式:
Regex wordCounter = new Regex (@"(w|[-'])+"); var query = dataContext.MedicalArticles .Where (article => article.Topic == "influenza" && wordCounter.Matches (article.Abstract).Count < 100);
问题是 SQL Server 不支持正则表达式,所以要用两步来解决这个问题,首先用 LINQ to SQL query 检索所有的流感文章,然后在本地筛选摘要少于 100 字的文章。
Regex wordCounter = new Regex (@"(w|[-'])+"); IEnumerable<MedicalArticle> sqlQuery = dataContext.MedicalArticles .Where (article => article.Topic == "influenza"); IEnumerable<MedicalArticle> localQuery = sqlQuery .Where (article => wordCounter.Matches (article.Abstract).Count < 100);
有了 AsEnumerable , 我们可以用一个查询步骤完成上面的操作。
Regex wordCounter = new Regex (@"(w|[-'])+"); var query = dataContext.MedicalArticles .Where (article => article.Topic == "influenza") .AsEnumerable() .Where (article => wordCounter.Matches (article.Abstract).Count < 100);
另一种调用 AsEnumerable 方法是 调用 ToArray 或 ToList。 AsEnumerable的优点是它不强制立即执行查询,也不创建任何存储结构。
将查询处理从数据库服务器转移到客户机可能会降低性能,特别是如果这意味着检索更多的行。
一个更高效的方法解决我们上面的示例,就是用 SQL CLR 集成 来公开一个在数据库上的函数,这个函数实现正则表达式。
小结:
解释查询与 本地查询的不同之处在于,它是对 IQuerable接口实现的序列进行操作,操作符也不是在原来的System.Linq.Enumerable类中实现的了,而是在System.Linq.Querable 中实现。这些操作符也不是接收一个委托,而是一个Expression 包装的委托。编译器会将这些操作符构成的 ”生产线 “ 转为表达式树,最终作为 SQL 传给数据库执行查询操作。