LINQ之路 8: 解释查询(Interpreted Queries)
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表达式的两种处理方式:
我们已经看到了lambda表达式可以被翻译成一个表达式树,那么他们有何作用呢?由于表达式树也是一个“普通”的对象,所以我们可以通过该对象的方法和属性来进行了解,下面是使用表达式树时的智能提示:
这些表达式树的成员让我们能够分析他们代表的代码以及用户的意图。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)的更多相关文章
- LINQ之路 7:子查询、创建策略和数据转换
在前面的系列中,我们已经讨论了LINQ简单查询的大部分特性,了解了LINQ的支持计术和语法形式.至此,我们应该可以创建出大部分相对简单的LINQ查询.在本篇中,除了对前面的知识做个简单的总结,还会介绍 ...
- LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法
本篇将是关于LINQ Operators的最后一篇,包括:集合运算符(Set Operators).Zip操作符.转换方法(Conversion Methods).生成器方法(Generation M ...
- LINQ之路10:LINQ to SQL 和 Entity Framework(下)
在本篇中,我们将接着上一篇“LINQ to SQL 和 Entity Framework(上)”的内容,继续使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术 ...
- LINQ之路 9:LINQ to SQL 和 Entity Framework(上)
在上一篇中,我们从理论和概念上详细的了解了LINQ的第二种架构“解释查询”.在这接下来的二个篇章中,我们将使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术的 ...
- LINQ之路 4:LINQ方法语法
书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询语法(Query Expression). LINQ方法语法是非常灵活和重要的,我们在这里将描述使用链接查询运算符的方 ...
- LINQ之路(3):LINQ扩展
本篇文章将从三个方面来进行LINQ扩展的阐述:扩展查询操作符.自定义查询操作符和简单模拟LINQ to SQL. 1.扩展查询操作符 在实际的使用过程中,Enumerable或Queryable中的扩 ...
- LINQ之路(2):LINQ to SQL本质
LINQ之路(2):LINQ to SQL本质 在前面一篇文章中回顾了LINQ基本语法规则,在本文将介绍LINQ to SQL的本质.LINQ to SQL是microsoft针对SQL Server ...
- LINQ之路15:LINQ Operators之元素运算符、集合方法、量词方法
本篇继续LINQ Operators的介绍,包括元素运算符/Element Operators.集合方法/Aggregation.量词/Quantifiers Methods.元素运算符从一个sequ ...
- LINQ之路14:LINQ Operators之排序和分组(Ordering and Grouping)
本篇继续LINQ Operators的介绍,这里要讨论的是LINQ中的排序和分组功能.LINQ的排序操作符有:OrderBy, OrderByDescending, ThenBy, 和ThenByDe ...
随机推荐
- dubbo源码分析6-telnet方式的管理实现
dubbo源码分析1-reference bean创建 dubbo源码分析2-reference bean发起服务方法调用 dubbo源码分析3-service bean的创建与发布 dubbo源码分 ...
- c3p0连接池]
<c3p0-config> <!-- 默认配置 --> <default-config> <property name="jdbcUrl" ...
- javaSE基础第二篇
1.JDK下载: www.oracle.com 2.JAVA_HOME bin目录:存放可执行文件.exe 把可能变的路径写入JAVA_HOME path=......;%JAVA_HOME%%; ...
- .NET组件控件实例编程系列——5.DataGridView数值列和日期列
在使用DataGridView编辑数据的时候,编辑的单元格一般会显示为文本框,逻辑值和图片会自动显示对应类型的列.当然我们自己可以手工选择列的类型,例如ComboBox列.Button列.Link列. ...
- yii2得到的数据对象转化成数组
yii2得到的数据对象转化成数组需要用到asArray().1.Customer::find(['id' => $id])->asArray()->one();2.$model = ...
- Android 短信广播接收相关问题
本人是Android新手,最近做了一个关于监听手机短信功能的应用,我在网上看资料了解到广播分为有序广播和无序广播,有序广播:无序广播又称普通广播,其中的利弊我也一时没搞清楚,我用的是有序广播实现的,具 ...
- 分享前端Facebook及Twitter第三方登录
最近公司要求做海外的第三方登录:目前只做了Facebook和Twitter;国内百度到的信息太少VPN FQ百度+Google了很久终于弄好了.但是做第三方登录基本上都有个特点就是引入必须的js,设置 ...
- C语言三维数组分解
很多人在学习C的时候,感觉三维数组很难想象,而且不理解深度是什么?做了一个图,帮大家分解一下 ...
- python--常见模块
本节大纲: 1.模块介绍 2.time&datetime 3.random. 4.os 5.sys 6.shutil 7.json&picle 8.shelve 9.xml处理 10. ...
- Mac OSX上的软件包管理工具,brew 即 Homebrew
brew 即 Homebrew,是Mac OSX上的软件包管理工具,能在Mac中方便的安装软件或者卸载软件, 只需要一个命令, 非常方便. brew类似ubuntu系统下的apt-get的功能. 安装 ...