LINQ学习笔记:解释查询(Interpreted Queries)
解释查询(Interpreted Queries)
LINQ提供了2个平行架构:针对本地对象集合的查询(local query), 以及针对远程数据源的查询(interpreted query). 目前我们它讨论的架构都是关于本地查询的, 主要是操作那些实现了IEnumerable<>的对象集合.而Interpreted query则是用于操作那些实现了IQueryable<>的序列, Queryable类中的查询操作符会通过执行运行时被解析的表达式树得到最终结果.
(Enumerable类中的查询操作符实际上也可以和IQueryable<>序列一起工作, 但问题是结果集只能在客户机本地运行才能得到 — 也这是为什么要在Queryable类中提供另外一套查询操作符的原因.)
当前的.NET Framework提供了两个IQueryable的具体实现:
1. LINQ to SQL
2. LINQ to Entities
另外, 扩展方法AsQueryable可以针对任何可枚举的集合生成一个IQueryable的包装.
假定我们将使用以下的SQL脚本创建一个简单的customer表并且插入一些名称:
1: create table Customer
2: (
3: ID int not null primary key,
4: Name varchar(30)
5: )
6: insert Customer values (1, 'James')
7: insert Customer values (2, 'Daniel')
8: insert Customer values (3, 'Carder')
9: insert Customer values (4, 'Amy')
10: insert Customer values (5, 'Joe')
这个表被创建以后, 我们可以编写在C#代码中编写一个Interpreted的LINQ查询用于查找那些名称中包含a字符的记录, 代码如下:
1: using System;
2: using System.Linq;
3: using System.Data.Linq;
4: using System.Data.Linq.Mapping;
5:
6: [Table] public class Customer
7: {
8: [Column(IsPrimaryKey=true)] public int ID;
9: [Column] public string Name;
10: }
11:
12: class Test
13: {
14: static void Main( )
15: {
16: var dataContext = new DataContext ("cx string... ");
17: Table<Customer> customers = dataContext.GetTable <Customer>( );
18:
19: IQueryable<string> query = from c in customers
20: where c.Name.Contains ("a")
21: orderby c.Name.Length
22: select c.Name.ToUpper( );
23:
24: foreach (string name in query)
25: Console.WriteLine (name);
26: }
27: }
LINQ to SQL将会把这个查询翻译成以下的SQL脚本:
1: SELECT UPPER([t0].[Name]) AS [value]
2: FROM [Customer] AS [t0]
3: WHERE [t0].[Name] LIKE '%a%'
4: ORDER BY LEN([t0].[Name])
并输出结果:
AMY
JAMES
DANIEL
CARDER
Interpreted查询是如何工作的?
让我们查看一下前面提到的查询是如何运行的.
首先, 编译器将这个复合查询转换成Lambda表达式语法. 这个跟本地查询的做法是一样的:
IQueryable<string> q = customers
.Where (n=> n.Name.Contains (”a”))
.OrderBy (n => n.Name.Length)
.Select (n => n.Name.ToUpper());
接下来,编译器将会解析查询操作方法, 这里就是本地查询和Interpreted查询的不同的地方了 – Interpreted查询将会使用Queryable类型中的查询方法而不是Enumerable类型中的.
要了解为什么, 我们需要查看一下customers变量, 这是创建整个查询的源头. Customers是一个Table<>类型, 其实现了IQueryable<>(这同时又是一个IEnumerable<>的子类型). 这意味着编译器在解析Where的时候有一个选择: 可能调用Enumerable中的扩展方法, 或者Queryable中的扩展方法:
1: public static IQueryable<TSource> Where<TSource> (
2: this IQueryable<TSource> source,
3: Expression <Func<TSource,bool>> predicate)
编译器最终选择Queryable.Where因为它的签名更加明确的匹配.
注意, Queryable.Where接受一个在Expression类型中的predicate包装, 这会指示编译器翻译Lambda表达式(n=>n.Name.Contains(“a”))到一个表达式树而不是一个编译的委托. 表达式树是一个基于System.Linq.Expressions的对象模型, 其可以在运行时被检视, 因此LINQ to SQL可以被翻译成SQL表达式.
执行
Interpreted查询遵循一个延迟执行模型,类似本地查询,这意味着只有当你开始枚举Query的时候SQL语句才会被生成. 并且, 枚举相同的Query两次将会对同样对数据库发起两次查询, 因此要小心避免引起性能问题.
Interpreted查询不同于本地查询的地方在于如何执行.当你开始枚举一个Interpreted查询的时候, 最外层的序列会运行一个程序横贯整个表达式树,将其处理成一个单元. 在我们的例子中,LINQ to SQL将表达式树翻译成SQL表达式, 然后运行并返回结果序列.
另外, 针对本地查询,我们可以非常方便的使用迭代器编写自己的扩展方法并将它们补充到之前定义的集合当中去. 但针对远程查询, 这是很困难的, 甚至是不合需要的. 如果你编写了一个MyWhere的IQueryable<>扩展方法, 这就像你将一个你自己的产品(比如手机)放到一条生产笔记本的生产线上, 生产线上的机器根本无法处理. 即使你在这个阶段进行干预, 你的解决方案你将与一个特殊的provider绑定在一起, (就像LINQ to SQL一样), 并且不能和其他的IQueryable的实现一起工作. Queryable拥有的一系列方法的好处之一就是他们定义了操作远程集合的标准词汇-_-, 不过当你试图扩展这些词汇时, 他们就不再是可互操作的.
此模型引起的另外一个结果是一个IQueryable Provider可能无法处理某些查询, 甚至是某些标准方法也是如此. 例如LINQ to SQL, 有些LINQ查询是无法对应的SQL语法的, 这取决于目标数据库服务器. 如果你对SQL非常熟悉, 你会对它们(那些可能无法翻译的Query)有个直观的感觉, 虽然通常情况下你可能需要进行一些实验才能了解到底是什么引起了运行时的错误, 那些翻译后的SQL语句可能会令你感到惊讶. 使用最新版本的SQL SERVER进行工作是你的最佳选择.
AsEnumerable
Enumerable.AsEnumerable是所有操作符中最简单的一个, 它的完整定义是:
1: public static IEnumerable AsEnumerable
2:
3: (this IEnumerable source)
4:
5: {
6:
7: return source;
8:
9: }
它的主要目的是将IQueryable转换成一个IEnumerable, 这将会强制将子序列绑定到Enumerable中的操作符而不是Queryable当中的.这意味着其后的执行都将会是在本地执行的.
举个例子, 假设我们有一个Article表存在于SQL SERVER当中, 我们想使用LINQ to SQL列出所有Topic等于LINQ并且Content包含超过500个字符的文章:
1: Regex reg = new Regex (@”\b(\w|[-’])+\b”);
2:
3: var query = dataContext.Articles
4:
5: .Where (article => article.Topic ==“LINQ” &&
6:
7: reg.Matches (article.Content).Count > 500);
这里的问题是SQL SERVER并不支持正则表达式, 因此上面的Query将无法运行. 为了解决这个问题, 我们可以将其分成2步查询:
1: Regex reg = new Regex (@”\b(\w|[’])+\b”);
2:
3: IEnumerable sqlQuery =
4:
5: dataContext.Articles
6:
7: .Where (a => a.Topic == “LINQ”);
8:
9: IEnumerable localQuery =sqlQuery
10:
11: .Where (a => reg.Matches (a.Content).Count > 500);
因为sqlQuery是一个IEnumerable类型, 其二次查询将会被绑定到本地的操作符上面, 并在本地运行.使用AsEnumerable, 我们可以将上述的两个查询合并成一个:
1: Regex reg= new Regex (@”\b(\w|[-’])+\b”);
2:
3: var query = dataContext.Articles
4:
5: .Where (a => a.Topic == “LINQ”)
6:
7: .AsEnumerable()
8:
9: .Where (a => reg.Matches (a.Content).Count > 500);
除了AsEnumerable, 我们还可以使用ToArray或者ToList, 而AsEnumerable的好处就是延迟执行, 并且不会创建任何的存储结构. 待续!