【原文地址】new "orcas" language feature: query syntax
【原文发表日期】 saturday, april 21, 2007 2:12
上个月我开始了一个贴子系列,讨论作为visual studio和.net框架orcas版本一部分发布的一些新的vb和c#语言特性。下面是该系列的前三篇贴子的链接:
今天的贴子要讨论另一个基础性的新语言特性:查询句法(query syntax)。
什么是查询句法(query syntax)?
查询句法是使用标准的linq查询运算符来表达查询时一个方便的声明式简化写法。该句法能在代码里表达查询时增进可读性和简洁性,读起来容易,也容易让人写对。visual studio 对查询句法提供了完整的intellisense和编译时检查支持。
在底下,c#和vb编译器则把查询句法的表达式翻译成明确的方法调用代码,这样的代码利用了orcas中的新的扩展方法和lambda表达式语言特性。
查询句法的例子:
在我以前的语言系列贴子里,我示范了你可以象下面这样声明一个person类:
然后我们可以使用下面这样的代码,用一些个人信息来生成一个list<person>集合实例,然后使用查询句法来对该集合做一个linq查询,只取出那些姓(last name)的首字母为g的人,按名字(first name)来排序(升序):
上面查询句法的表达式在语意上与下面明确使用linq扩展方法和lambda表达式的代码是等同的:
使用查询句法方法的好处是,结果会是稍微容易读写些,这在表达式变得更繁复时尤其如此。
查询句法 - 理解from和select子句:
在c#中,每个查询表达式的句法从from子句开始,以select或group子句结束。from子句表示你要查询什么数据。select子句则表示你要返回什么数据,且应该以什么构形返回。
譬如,让我们再来看一下我们对list<person>集合的查询:
在上面的代码片段里,"from p in people"表示了我要对"people" 这个集合做一个linq查询,我将用参数"p"代表我正查询的输入序列的每个项。我们将参数命名为"p" 这个事实是无关紧要的,我完全可以很容易地将其命名为"o", "x", "person"或我想要的任何名字。
在上面的代码片段里,语句结尾的"select p"子句表示,作为查询的结果,我要返回一个person对象的ienumerable序列。这是因为"people"集合包含了person类型的对象,而参数p则代表了输入序列中的person对象。因此,该查询句法表达式的结果数据类型是ienumerable<person>。
假如不是返回person对象,我想返回该集合中的人的名字,我可以把查询改写成这样:
注意上面我不再说"select p",而是说"select p.firstname"。这表示我不想返回一串person对象,而是想返回一串字符串,由person对象的firstname属性(该属性是个字符串)填充而来。 因此,该查询句法表达式的结果类型是 ienumerable<string>。
针对数据库的查询句法的例子
linq的妙处在于,我可以针对任何数据类型使用完全一样的查询句法。譬如,我可以使用orcas提供的新linq到sql对象关系映射器支持,对sql服务器的northwind数据库进行建模,生成下面这些类(请观看我这里的录像来学习该如何实现):
在上面定义好类模型之后(以及它与数据库间的映射关系),然后我就可以写个查询句法的表达式取出那些单价大于99元的产品:
在上面的代码片段里,我表示我要对northwinddatacontext类的products表进行一个linq查询,northwinddatacontext类是由visual studio orcas的orm设计器生成的。"select p"表示我要返回匹配我的查询的一串product对象,因此,该查询句法表达式的结果数据类型是ienumerable<product>。
就象前面list<person>查询句法的例子一样,c# 编译器会把我们的声明式查询句法翻译成明确的扩展方法调用(使用lambda表达式作为参数)。在上面的linq到sql的例子的情形下,这些lambda表达式会被转化成sql命令,然后在sql服务器上做运算(这样,只有那些匹配查询条件的product记录行会返回到我们的应用中)。促成这个lambda->sql 转化的机制的细节可见于我的lambda表达式博客贴子的"lambda表达式树"部分。
查询句法 - 理解where和orderby子句:
在一个查询句法表达式开头的"from" 子句和结尾的"select"子句之间,你可以使用最常见的linq查询运算符来过滤和转换你在查询的数据。两个最常用的子句是"where"和"orderby"。这两个子句处理对结果集的过滤和排序。
譬如,要从northwind数据库里返回按字母降序排列的分类名称列表,过滤条件是只包括那些含有5个以上产品的分类,我们可以编写下面这样的查询句法来用linq到sql对我们的数据库做查询:
在上面的表达式里,我们加了 "where c.products.count > 5" 子句来表示我们只要那些含有5个以上产品的分类。这利用了数据库中产品和分类间的linq到sql的orm映射的关联。在上面的表达式中,我也加了"order by c.categoryname descending"子句来表示我要将结果集按名称降序排列。
linq到sql然后就会在使用这个表达式查询数据库时,生成下列sql:
select [t0].[categoryname] from [dbo].[categories] as [t0]
where ((
select count(*)
from [dbo].[products] as [t1]
where [t1].[categoryid] = [t0].[categoryid]
)) > 5
order by [t0].[categoryname] desc
注意,linq到sql很聪明,只返回了我们所需的单个字段(分类名称), 而且它是在数据库层做了所有的过滤和排序,使得该查询效率非常高。
查询句法 - 用投影(projection)来转换数据
先前我指出的一个要点是,"select" 子句表示了你要返回的数据,以及这个数据的构形是什么。
譬如,假如你有个象下面这样的"select p" 子句,这里p的类型是person,然后,它就会返回一串person对象:
linq和查询句法提供的一个非常强大的功能是允许你定义跟被查询的数据分开的新的类型,然后用新的类型来控制查询返回的数据的形状和结构。
譬如,假设我们定义了一个新的alternateperson类,内含一个fullname属性,而不是我们原先的person类内的分开的firstname和lastname属性:
然后我就可以使用下面的linq查询句法来查询我原先的list<person>集合,用下面的查询句法将结果转换成一串alternateperson对象:
注意看,我们是如何在上面的表达式里的"select"子句里,使用我的语言系列的第一个贴子里讨论过的新的对象初始化器句法来创建新的alternateperson实例,同时设置它的属性的。也注意我是如何连接我们原先person类的firstname和lastname属性,然后将其赋值给fullname属性的。
对数据库使用查询句法投影
这个投影特性在操作从象数据库这样一个远程数据提供器那里取回的数据时,会变得难以置信地有用,因为它提供给我们一个优雅的方式,来表示我们的orm应该从数据库实际取回哪些数据字段。
譬如,假设我用了linq到sql的orm提供器对northwind数据库建模,生成下面这些类:
通过编写下面这个linq查询,我告诉linq到sql我要返回一串product对象:
填充product类所需的所有字段都将作为上面查询的一部分从数据库中返回,由linq到sql orm执行的raw sql看上去象下面这样:
select [t0].[productid], [t0].[productname], [t0].[supplierid], [t0].[categoryid],
[t0].[quantityperunit], [t0].[unitprice], [t0].[unitsinstock],
[t0].[unitsonorder], [t0].[reorderlevel], [t0].[discontinued]
from [dbo].[products] as [t0]
where [t0].[unitprice] > 99
在一些场景下,我不需要也不用所有这些字段,我可以定义一个下面这样的新的myproduct类,只拥有product类具有的部分属性,以及一个product类并不具有的额外属性,totalrevenue (注: 对那些不熟悉c#的,decimal?句法表示我们的unitprice属性是个nullable值):
然后我就可以使用下面这个查询,使用查询句法的投影功能来构造我要从数据库返回的数据的形状:
这表明,不是返回一串product对象,我要myproduct对象,我只要其中三个属性被赋值,linq到sql就会很聪明地调整要执行的raw sql语句,从数据库只返回那三个需要的产品字段:
select [t0].[productid], [t0].[productname], [t0].[unitprice]
from [dbo].[products] as [t0]
where [t0].[unitprice] > 99
为炫耀起见,我也可以填充myproduct类的第四个属性,即totalrevenue属性。我要这个值等于我们产品目前的销售额的总量。这个值在northwind数据库中并没有作为一个预先算好的字段而存在。而是,你需要在products表和order details表间做一个关联,然后计算出一个给定产品对应的所有的order detail 行的总量。
非常酷的是,我可以在product类的orderdetails关联上使用linq的 sum 这个扩展方法,编写一个作为我的查询句法投影一部分的乘法lambda表达式,来计算这个值:
linq到sql就会非常聪明地使用下面这个sql在sql数据库里做运算:
select [t0].[productid], [t0].[productname], [t0].[unitprice], (
select sum([t2].[value])
from (
select [t1].[unitprice] * (convert(decimal(29,4),[t1].[quantity])) as [value], [t1].[productid]
from [dbo].[order details] as [t1]
) as [t2]
where [t2].[productid] = [t0].[productid]
) as [value]
from [dbo].[products] as [t0]
where [t0].[unitprice] > 99
查询句法 - 理解延迟执行(deferred execution)和使用tolist() 和toarray()
在默认情形下,查询句法表达式的结果的类型是ienumerable<t>。在上面的例子里,你会注意到所有的查询句法赋值是给ienumerable<product>, ienumerable<string>, ienumerable<person>, ienumerable<alternateperson>, 和 ienumerable<myproduct> 变量的。
ienumerable<t>接口的一个很好的特征是,实现它们的对象可以把实际的查询运算延迟到开发人员第一次试图对返回值进行迭代(这是通过使用最早在vs 2005中c# 2.0 中引进的yield构造来达成的)时才进行。linq和查询句法表达式利用了这个特性,将查询的实际运算延迟到了你第一次对返回值进行循环时才进行。假如你对ienumerable<t>的结果从不进行迭代的话,那么查询根本就不会执行。
譬如,考虑下面这个linq到sql的例子:
不是在查询句法表达式声明的时候,而是在我们第一次试图对结果进行循环(上面红箭头标志的地方),才会去访问数据库以及取出填充category对象所需的值。
这个延迟运算的行为结果变得非常有用,因为它促成了一些把多个linq查询和表达式链在一起的强有力的组合场景。譬如,我们可以把一个表达式的结果喂给另一个表达式,然后通过延迟运算,允许象linq 到sql这样的orm根据整个表达式树来优化raw sql。我将在以后的一个博客贴子里对这样的场景做示范说明。
如何立刻对查询句法表达式做运算
如果你不要延迟查询运算,而是要对它们立刻就执行运算,你可以使用内置的tolist() 和toarray() 运算符来返回一个包括了结果集的list<t>或者数组。
譬如,要返回一个基于范型的 list<t> 集合的话:
要返回一个数组的话:
在上面两种情形下,会立刻访问数据库,填充category对象。
结语
查询句法在使用标准的linq查询运算符来表达查询时,提供了非常方便的声明式简化写法。它提供的句法可读性非常高,可以针对任何类型的数据(内存中的集合,数组,xml内容,以及象数据库这样的远程数据提供器,web服务等等)进行查询。一旦你熟悉这个句法后,你可以在任何地方应用这个知识。
在不远的将来,我将结束本语言系列的最后一部分,该部分将讨论新的匿名类型特性。然后我将转而讨论在实际应用中使用所有这些语言特性的一些非常实用的例子(特别是针对数据库和xml文件使用linq的例子)。
希望本文对你有所帮助,
scott