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 + 1, 42)

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

        // 对于函数调用
Calculate(x => x + 1, 42);

// 如果参数为委托类型
int Calculate(Func<int, int> op, int arg);

// 这时编译器会为lambda表达式生成等价的匿名方法:
Calculate(delegate (int x) { return x + 1; }, 42);

//我们在Calculate方法里面可以通过委托调用语法来调用该匿名方法:
int Calculate(Func<int, int> op, int arg)
{
return op(arg);
}

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

        // 对于函数调用
Calculate(x => x + 1, 42);

// 如果参数为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(1)
),
x
);
Calculate(f, 42);

下面这幅图更加直观的比较了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 > 0)
{
yield return new SplittedName { FirstName = name.Substring(0, index), LastName = name.Substring(index + 1) };
}
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 < 100);

但上面的查询并不能成功运行,因为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 < 100);

通过使用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 < 100);

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

posted @ 2011-11-07 10:49  Life a Poem  阅读(12706)  评论(54编辑  收藏  举报