LINQ之路 8: 解释查询(Interpreted Queries)

时间:2022-09-01 07:50:17

LINQ提供了两个平行的架构:针对本地对象集合的本地查询(local queries),以及针对远程数据源的解释查询(Interpreted queries)。

在讨论LINQ to SQL等具体技术之前,我们有必要先对这两种架构进行了解和学习,只有在完全理解了他们的特点和原理后,才能够在LINQ to SQL等的学习过程中做到知其然且知其所以然,才能充分利用本地查询和解释查询的各自优势,写出高效正确的LINQ查询。本篇目的就是试图对解释查询的工作方式和实现原理进行剖析。

简单回忆一下之前我们讨论的本地查询架构,它用来操作实现了IEnumerable<T>的对象集合。本地查询对应Enumerable类的查询运算符,返回装饰sequence以支持延迟执行。在创建本地查询时提供的lambda表达式最终会生成对应IL代码,就像其它C#方法那样。

而解释查询用来操作实现了IQueryable<T>的sequence,并对应Queryable类中的查询运算符,这些运算符会生成运行时能被检测的表达式树,相应的LINQ Provider通过分析表达式树最终得到查询结果。

当前,.NET Framework提供了IQueryable<T>的两个具体实现:LINQ to SQL、Entity Framework(EF)。这些LINQ-to-db技术对LINQ查询的支持非常类似,所以我们写出的查询一般会同时适用于LINQ to SQL和EF。

IQueryable<T>泛型借口继承自IEnumerable<T>,并添加了新的方法用来构造表达式树。通常来讲,系统会间接而透明的调用他们,我们可以不用理会。

下面这个简单的示例假设我们在SQL Server中创建了Customer表并填充了几行数据:

create table Customer
(
ID int not null primary key,
Name varchar(30)
) insert Customer values(1, 'Tom Chen')
insert Customer values(2, 'Vincent Ke')
insert Customer values(3, 'Alan' )
insert Customer values(4, 'Jay Heyssi')
insert Customer values(5, 'Daisy Liu')

现在,我们可以创建LINQ query来查询包含字母”a”的Employee了:

    [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([to].[Name]) AS [value]
FROM [Employee] AS [to]
WHERE [to].[Name] LIKE @p0
ORDER BY LEN([to].[Name])

最终得到如下结果:

ALAN
DAISY LIU
JAY HEYSSI

解释查询的工作方式

让我们来仔细的了解一下上面query的运行过程:首先,编译器会把查询表达式转换成方法语法,这一点和本地查询完全一致,转换后的查询如下:

            IQueryable<string> query = customers
.Where(n => n.Name.Contains("a"))
.OrderBy(n => n.Name.Length)
.Select(n => n.Name.ToUpper());

接下来,编译器将会解析查询操作方法。这里就是本地查询和解释查询不同的地方了,解释查询将会使用Queryable类中的查询运算符而不是Enumerable类。因为employees的类型是Table<>,它实现了IQueryable<T>接口(IQueryable<T>进而继承自IEnumerable<T>)。编译器为employees.Where选择了Queryable类中的扩展方法是因为它的签名具有更加确切的类型匹配:

        public static IQueryable<TSource> Where<TSource>(
this IQueryable<TSource> source, Expression <Func<TSource, bool>> predicate)

Queryable.Where方法接受的predicate参数类型为Expression <Func<TSource, bool>>型,它指示编译器将提供的lambda表达式(e => e.Name.Contains(“a”))翻译成一个表达式树而不是一个编译的委托方法。表达式树是一个基于System.Linq.Expressions中类型的对象模型,需要知道的是,表达式树并不包含代码的执行结果,而只是代码的数据表现形式。并且表达式树可以在运行时被检测,因此LINQ to SQL可以将其翻译成SQL查询语句。

因为Queryable.Where方法也是返回IQueryable<T>,所以我们可以像本地查询那样在后面链接其它查询运算符,如OrderBy、Select等,他们的处理方式与Where一样。这样,查询的最终结果是一个描述了整个查询的表达式树。

表达式树和Lambda表达式的同像性(Homoiconicity)

那么表达式树是如何生成的呢?答案是C#语言(从3.0开始)为lambda表达式提供的同像性功能,该特性通常存在于函数式编程语言LISP中,这意味着lambda表达式使用相同的语法形式来表示代码(IL指令)和数据表示(表达式树)。比如下面的代码,我们无法确定编译器如何翻译该lambda表达式:

        Calculate(x => x + , )

我们只有在查看接收该lambda表达式的参数声明后,才能知道编译器的处理方式。这里有两种可能:第一种就是委托参数,如下所示:

        // 对于函数调用
Calculate(x => x + , ); // 如果参数为委托类型
int Calculate(Func<int, int> op, int arg); // 这时编译器会为lambda表达式生成等价的匿名方法:
Calculate(delegate (int x) { return x + ; }, ); //我们在Calculate方法里面可以通过委托调用语法来调用该匿名方法:
int Calculate(Func<int, int> op, int arg)
{
return op(arg);
}

然而,如果lambda表达式赋予的不是一个委托类型而是一个Expression<TDelegate>,编译器将会为lambda表达式生成表达式树,如下所示:

        // 对于函数调用
Calculate(x => x + , ); // 如果参数为Expression<Func<int, int>>类型
int Calculate(Expression<Func<int, int>> op, int arg); // 编译器将会为之生成如下等价代码
var x = Expression.Parameter(typeof(int), “x”);
var f = Expression.Lambda<Func<int, int>>(
Expression.Add(
x,
Expression.Constant()
),
x
);
Calculate(f, );

下面这幅图更加直观的比较了lambda表达式的两种处理方式:

LINQ之路 8: 解释查询(Interpreted Queries)

我们已经看到了lambda表达式可以被翻译成一个表达式树,那么他们有何作用呢?由于表达式树也是一个“普通”的对象,所以我们可以通过该对象的方法和属性来进行了解,下面是使用表达式树时的智能提示:

LINQ之路 8: 解释查询(Interpreted Queries)

这些表达式树的成员让我们能够分析他们代表的代码以及用户的意图。LINQ Provider最终将之转换成领域专用的查询语言比如SQL,被转换的SQL被发送到相应的数据库服务器,得到LINQ查询的结果。

解释查询的执行

与本地查询一样,解释查询也是延迟执行的。这意味着直到我们真正遍历查询结果时,相应的SQL语句才会生成。并且,如果多次遍历查询会导致对数据库的多次查询,所以要注意由此带来的性能问题。比如:

            DataContext context = new DataContext("connection string");                        // 谢谢园友 A_明~坚持 提供了此示例
context.Log = new StreamWriter(@"D:\Documents\Blog\Linq2Sql.log", true) { AutoFlush = true }; // Append to & Auto Flush the log file
var query = from n in context.GetTable<Purchase>() select n.Price; int count = query.Count(); // 上面的查询第一次被执行
decimal average = query.Average(); // 第二次
decimal sum = query.Sum(); // 第三次

解释查询不同于本地查询的地方在于它的执行方式。当我们开始枚举一个解释查询时,最外层的sequence会运行一个程序来遍历整个表达式树,并将其处理成一个单元。在我们的例子中,LINQ to SQL将表达式树翻译成SQL查询语句,然后运行并返回结果序列。而本地查询会针对每个查询运算符调用相应的扩展方法,形成一个执行链。

尽管我们可以非常方便的使用迭代器编写自己的扩展方法来对本地查询进行扩展,但解释查询的执行方式使得我们很难对IQueryable<>进行扩展,因为各个LINQ Provider对表达式树的处理是不一样的,这样的好处是Queryable的一系列方法定义了查询远程数据源的标准词汇。解释查询的另一个问题是:一个IQueryable Provider可能无法处理某些查询,甚至是对标准查询运算符也是如此。例如LINQ to SQL和EF都会受到目标数据库服务器的限制,一个例子是SQL Server不支持正则表达式的使用。

组合使用解释查询和本地查询

一个LINQ查询可以同时包含解释查询和本地查询运算符,通常,我们先使用解释查询获取数据,然后使用本地查询做进一步的处理,这个模式非常适用于LINQ-to-database查询。

比如针对上面的示例,我们定义了如下的扩展方法来解析姓名中的FirstName和LastName:

    public class SplittedName
{
public string FirstName;
public string LastName;
} public static IEnumerable<SplittedName> SplitName(this IEnumerable<string> source)
{
foreach (string name in source)
{
int index = name.LastIndexOf(" ");
if (index > )
{
yield return new SplittedName { FirstName = name.Substring(, index), LastName = name.Substring(index + ) };
}
else
{
yield return new SplittedName { FirstName = name, LastName = "" };
}
}
}

我们可以使用上面的扩展方法来组合LINQ to SQL和本地查询运算符:

        static void TestInterpretedQuery()
{
DataContext dataContext = new DataContext("Data Source=localhost; Initial Catalog=test; Integrated Security=SSPI;");
Table<Customer> customers = dataContext.GetTable<Customer>();
IEnumerable<string> query = customers
.Select(n => n.Name.ToUpper())
.OrderBy(n => n)
.SplitName() // 从这里开始就是本地查询了
.Select((n, i) => "First Name " + i.ToString() + " = " + n.FirstName); foreach (string element in query) Console.WriteLine(element);
}

因为customer是实现了IQueryable<T>的类型,所以customer.Select对应Queryable.Select并返回IQueryable<T>类型。直到遇到自定义的运算符SplitName,因为它只有针对IEnumerable<>的版本,所以它被解析到我们自定义的本地SplitName,从而将一个解释查询包装在本地查询里。对LINQ to SQL来讲,最终生成的SQL语句如下:

SELECT UPPER(Name) FROM Customer ORDER BY UPPER(Name)

剩下的工作则在本地完成,LINQ to Objects接管了余下的工作。换句话说,我们创建了一个本地查询,它的数据源来自一个解释查询。

AsEnumerable

Enumerable.AsEnumerable是所有查询运算符中最简单的一个,它的完整定义如下:

        public static IEnumerable<TSource> AsEnumerable<TSource>(
this IEnumerable<TSource> source)
{
return source;
}

它的目的是将IQueryable<T> sequence转换成一个IEnumerable,这将会强制将后续的查询运算符绑定到Enumerable类而不是Queryable,意味着其后的执行都将会是在本地执行的。

举个例子, 假设我们SQL Server中有一个Article表,我们想使用LINQ to SQL列出所有Topic等于LINQ并且Abstract小于100个字符的文章,我们会写出如下的查询:

            Regex wordCounter = new Regex(@"\b(\w|[-'])+\b");
var query = articles
.Where(article => article.Topic == "LINQ" &&
wordCounter.Matches(article.Abstract).Count < );

但上面的查询并不能成功运行,因为SQL SERVER并不支持正则表达式。为了解决这个问题,我们可以将其分成2步查询:

            Regex wordCounter = new Regex(@"\b(\w|[-'])+\b");
IEnumerable<Article> sqlQuery = articles.Where(article => article.Topic == "LINQ"); //注意返回类型为IEnumerable<> IEnumerable<Article> localQuery = sqlQuery // 因为sqlQuery类型是IEnumerable<>,所以这是一个本地查询
.Where(article => wordCounter.Matches(article.Abstract).Count < );

通过使用AsEnumerable,我们可以将上面的两个查询合二为一:

            Regex wordCounter = new Regex(@"\b(\w|[-'])+\b");
var sqlQuery = articles
.Where(article => article.Topic == "LINQ")
.AsEnumerable() // 把IQueryable<>转换成IEnumerable<>
.Where(article => wordCounter.Matches(article.Abstract).Count < );

除了AsEnumerable,我们还可以使用ToArray或者ToList来把一个解释查询转换成本地查询,而AsEnumerable的好处就是延迟执行,并且不会创建任何的存储结构。

LINQ之路 8: 解释查询(Interpreted Queries)的更多相关文章

  1. LINQ之路 7:子查询、创建策略和数据转换

    在前面的系列中,我们已经讨论了LINQ简单查询的大部分特性,了解了LINQ的支持计术和语法形式.至此,我们应该可以创建出大部分相对简单的LINQ查询.在本篇中,除了对前面的知识做个简单的总结,还会介绍 ...

  2. LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法

    本篇将是关于LINQ Operators的最后一篇,包括:集合运算符(Set Operators).Zip操作符.转换方法(Conversion Methods).生成器方法(Generation M ...

  3. LINQ之路10:LINQ to SQL 和 Entity Framework(下)

    在本篇中,我们将接着上一篇“LINQ to SQL 和 Entity Framework(上)”的内容,继续使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术 ...

  4. LINQ之路 9:LINQ to SQL 和 Entity Framework(上)

    在上一篇中,我们从理论和概念上详细的了解了LINQ的第二种架构“解释查询”.在这接下来的二个篇章中,我们将使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术的 ...

  5. LINQ之路 4:LINQ方法语法

    书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询语法(Query Expression). LINQ方法语法是非常灵活和重要的,我们在这里将描述使用链接查询运算符的方 ...

  6. LINQ之路(3):LINQ扩展

    本篇文章将从三个方面来进行LINQ扩展的阐述:扩展查询操作符.自定义查询操作符和简单模拟LINQ to SQL. 1.扩展查询操作符 在实际的使用过程中,Enumerable或Queryable中的扩 ...

  7. LINQ之路(2):LINQ to SQL本质

    LINQ之路(2):LINQ to SQL本质 在前面一篇文章中回顾了LINQ基本语法规则,在本文将介绍LINQ to SQL的本质.LINQ to SQL是microsoft针对SQL Server ...

  8. LINQ之路15:LINQ Operators之元素运算符、集合方法、量词方法

    本篇继续LINQ Operators的介绍,包括元素运算符/Element Operators.集合方法/Aggregation.量词/Quantifiers Methods.元素运算符从一个sequ ...

  9. LINQ之路14:LINQ Operators之排序和分组&lpar;Ordering and Grouping&rpar;

    本篇继续LINQ Operators的介绍,这里要讨论的是LINQ中的排序和分组功能.LINQ的排序操作符有:OrderBy, OrderByDescending, ThenBy, 和ThenByDe ...

随机推荐

  1. FMDB 多线程使用

    在App中保持一个FMDatabaseQueue的实例,并在所有的线程中都只使用这一个实例. [FMDatabaseQueue databaseQueueWithPath:path]; FMDatab ...

  2. Android-Universal-Image-Loader 图片异步加载类库的使用(超详细配置)

    这个图片异步加载并缓存的类已经被很多开发者所使用,是最常用的几个开源库之一,主流的应用,随便反编译几个火的项目,都可以见到它的身影. 可是有的人并不知道如何去使用这库如何进行配置,网上查到的信息对于刚 ...

  3. WAMP环境下访问PHP提示下载PHP文件

    原因是服务器没有加载到PHP文件 到http.conf下加载 AddType application/x-httpd-php .php AddType application/x-httpd-php ...

  4. 《OD学hadoop》第一周0625

    一.实用网站 1. linux内核版本 www.kernel.org 2. 查看网站服务器使用的系统  www.netcraft.com 二.推荐书籍 1. <Hadoop权威指南> 1- ...

  5. 【转】android新建项目时 出现appcompat&lowbar;v7工程错误和红色感叹号

    原文网址:http://www.cnblogs.com/xiaozhang2014/p/4109856.html 最近初学android,版本是22.6.0的话,每次创建一个项目就会出现一个appco ...

  6. COJ 0580 4021征兵方案

    4021征兵方案 难度级别: C: 编程语言:不限:运行时间限制:1000ms: 运行空间限制:51200KB: 代码长度限制:2000000B 试题描述 现在需要征募女兵N人,男兵M人,每征募一个人 ...

  7. Linux:alias永久生效

    alias(中文称为"别名")允许使用更加简短的名称来重新定义 Linux 中的 Shell 命令,从而简化命令行的输入. 如果经常与 CLI 打交道,那么使用 alias 不仅会 ...

  8. zabbix 监控报警详细邮件内容

    AlarmHost:{HOSTNAME1} AlarmTime:{EVENT.DATE} {EVENT.TIME} AlarmLevel:{TRIGGER.SEVERITY} AlarmMessige ...

  9. &lpar;转&rpar; IDirectSoundBuffer&colon;&colon;SetVolume的参数与音量分贝的函数关系

    假如将播放器的控制音量切割成0-100的话,由于IDirectSoundBuffer::SetVolume(LONG lVolume)中参数的输入值是[-10000,0] MySetVolume( D ...

  10. swift pop实现动感按钮动画

    // //  MyButton.swift //  PopInstall // //  Created by su on 15/12/11. //  Copyright © 2015年 tian. A ...