4 、Interpreted Queries
LINQ提供了两种并行架构:
-
本地对象集合的本地查询
-
远程数据源的解释查询。
到目前为止,我们研究的都是本地查询的体系结构,它们操作在实现了 IEnumerable
解释查询是描述性的
解释查询不对 IEnumerable接口实现的序列操作了,它们对实现iquerizable
Enumerable中的查询操作符实际上可以使用iquerizable
There are two IQueryable
-
LINQ to SQL
-
Entity Framework (EF)
It’s also possible to generate an IQueryable
(翻译为汉语还有点别扭)可以通过调用 AsQueryable 方法来生成一个普通可枚举集合的iquerable
我们先用 LINQ to SQL 说明 interpreted query 架构,因为 LINQ to SQL 不需要 Entity Data Model。 但是,我们编写的查询在 Entity Framework(以及许多第三方产品)上同样有效。
IQuerizable
假设我们在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"); Tablecustomers = dataContext.GetTable (); 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
public static IQueryableWhere (this IQueryable source, Expression bool>> predicate)
编译器选择 Queryable.where 因为它的签名是更具体的匹配。
Querable.where 接收一个谓词,这个谓词被封装在一个 Expression
一个表达式树是一个对象模型,这个模型基于在 System.Linq.Expressions 中的类型,而它可以在运行时被检查。这样 LINQ to SQL or EF 以后可以把它转换成SQL语句。
因为 Queryable.Where 返回一个 IQueryable
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"); Tablecustomers = dataContext.GetTable (); 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
剩下的工作在本地完成。实际上,我们最终得到一个本地查询(外部),它的源是一个解释查询(内部)。
AsEnumerable
Enumerable.AsEnumerable 是所有 query 操作符中最简单的一个。下面是它的完整定义。
public static IEnumerableAsEnumerable (this IEnumerable source) { return source; }
它的目的是将IQuerable
假设我们有一个 MedicalArticles 表在 SQL Server 中,想用 LINQ to SQL or EF 来检索所有摘要少于100字的流感文章, 对于后一个谓词,我们需要一个正则表达式:
Regex wordCounter = new Regex (@"\b(\w|[-'])+\b"); 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 (@"\b(\w|[-'])+\b"); IEnumerablesqlQuery = dataContext.MedicalArticles .Where (article => article.Topic == "influenza"); IEnumerable localQuery = sqlQuery .Where (article => wordCounter.Matches (article.Abstract).Count < 100);
有了 AsEnumerable , 我们可以用一个查询步骤完成上面的操作。
Regex wordCounter = new Regex (@"\b(\w|[-'])+\b"); 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 传给数据库执行查询操作。