【原文地址】New "Orcas" Language Feature: Query Syntax
【原文發(fā)表日期】 Saturday, April 21, 2007 2:12
上個月我開始了一個貼子系列,討論作為Visual Studio和.NET框架Orcas版本一部分發(fā)布的一些新的VB和C#語言特性。下面是該系列的前三篇貼子的鏈接:
- 自動屬性,對象初始化器,和集合初始化器
- 擴(kuò)展方法
- Lambda表達(dá)式
今天的貼子要討論另一個基礎(chǔ)性的新語言特性:查詢句法(Query Syntax)。
什么是查詢句法(Query Syntax)?
查詢句法是使用標(biāo)準(zhǔn)的LINQ查詢運(yùn)算符來表達(dá)查詢時一個方便的聲明式簡化寫法。該句法能在代碼里表達(dá)查詢時增進(jìn)可讀性和簡潔性,讀起來容易,也容易讓人寫對。Visual Studio 對查詢句法提供了完整的intellisense和編譯時檢查支持。
在底下,C#和VB編譯器則把查詢句法的表達(dá)式翻譯成明確的方法調(diào)用代碼,這樣的代碼利用了Orcas中的新的擴(kuò)展方法和Lambda表達(dá)式語言特性。
查詢句法的例子:
在我以前的語言系列貼子里,我示范了你可以象下面這樣聲明一個Person類:
然后我們可以使用下面這樣的代碼,用一些個人信息來生成一個ListPerson>集合實(shí)例,然后使用查詢句法來對該集合做一個LINQ查詢,只取出那些姓(last name)的首字母為G的人,按名字(first name)來排序(升序):
上面查詢句法的表達(dá)式在語意上與下面明確使用LINQ擴(kuò)展方法和Lambda表達(dá)式的代碼是等同的:
使用查詢句法方法的好處是,結(jié)果會是稍微容易讀寫些,這在表達(dá)式變得更繁復(fù)時尤其如此。
查詢句法 - 理解from和select子句:
在C#中,每個查詢表達(dá)式的句法從from子句開始,以select或group子句結(jié)束。from子句表示你要查詢什么數(shù)據(jù)。select子句則表示你要返回什么數(shù)據(jù),且應(yīng)該以什么構(gòu)形返回。
譬如,讓我們再來看一下我們對ListPerson>集合的查詢:
在上面的代碼片段里,"from p in people"表示了我要對"people" 這個集合做一個LINQ查詢,我將用參數(shù)"p"代表我正查詢的輸入序列的每個項(xiàng)。我們將參數(shù)命名為"p" 這個事實(shí)是無關(guān)緊要的,我完全可以很容易地將其命名為"o", "x", "person"或我想要的任何名字。
在上面的代碼片段里,語句結(jié)尾的"select p"子句表示,作為查詢的結(jié)果,我要返回一個Person對象的IEnumerable序列。這是因?yàn)?people"集合包含了Person類型的對象,而參數(shù)p則代表了輸入序列中的Person對象。因此,該查詢句法表達(dá)式的結(jié)果數(shù)據(jù)類型是IEnumerablePerson>。
假如不是返回Person對象,我想返回該集合中的人的名字,我可以把查詢改寫成這樣:
注意上面我不再說"select p",而是說"select p.FirstName"。這表示我不想返回一串Person對象,而是想返回一串字符串,由Person對象的FirstName屬性(該屬性是個字符串)填充而來。 因此,該查詢句法表達(dá)式的結(jié)果類型是 IEnumerablestring>。
針對數(shù)據(jù)庫的查詢句法的例子
LINQ的妙處在于,我可以針對任何數(shù)據(jù)類型使用完全一樣的查詢句法。譬如,我可以使用Orcas提供的新LINQ到SQL對象關(guān)系映射器支持,對SQL服務(wù)器的Northwind數(shù)據(jù)庫進(jìn)行建模,生成下面這些類(請觀看我這里的錄像來學(xué)習(xí)該如何實(shí)現(xiàn)):
在上面定義好類模型之后(以及它與數(shù)據(jù)庫間的映射關(guān)系),然后我就可以寫個查詢句法的表達(dá)式取出那些單價大于99元的產(chǎn)品:
在上面的代碼片段里,我表示我要對NorthwindDataContext類的Products表進(jìn)行一個LINQ查詢,NorthwindDataContext類是由Visual Studio orcas的ORM設(shè)計(jì)器生成的。"select p"表示我要返回匹配我的查詢的一串Product對象,因此,該查詢句法表達(dá)式的結(jié)果數(shù)據(jù)類型是IEnumerableProduct>。
就象前面ListPerson>查詢句法的例子一樣,C# 編譯器會把我們的聲明式查詢句法翻譯成明確的擴(kuò)展方法調(diào)用(使用Lambda表達(dá)式作為參數(shù))。在上面的LINQ到SQL的例子的情形下,這些Lambda表達(dá)式會被轉(zhuǎn)化成SQL命令,然后在SQL服務(wù)器上做運(yùn)算(這樣,只有那些匹配查詢條件的Product記錄行會返回到我們的應(yīng)用中)。促成這個Lambda->SQL 轉(zhuǎn)化的機(jī)制的細(xì)節(jié)可見于我的Lambda表達(dá)式博客貼子的"Lambda表達(dá)式樹"部分。
查詢句法 - 理解where和orderby子句:
在一個查詢句法表達(dá)式開頭的"from" 子句和結(jié)尾的"select"子句之間,你可以使用最常見的LINQ查詢運(yùn)算符來過濾和轉(zhuǎn)換你在查詢的數(shù)據(jù)。兩個最常用的子句是"where"和"orderby"。這兩個子句處理對結(jié)果集的過濾和排序。
譬如,要從Northwind數(shù)據(jù)庫里返回按字母降序排列的分類名稱列表,過濾條件是只包括那些含有5個以上產(chǎn)品的分類,我們可以編寫下面這樣的查詢句法來用LINQ到SQL對我們的數(shù)據(jù)庫做查詢:
在上面的表達(dá)式里,我們加了 "where c.Products.Count > 5" 子句來表示我們只要那些含有5個以上產(chǎn)品的分類。這利用了數(shù)據(jù)庫中產(chǎn)品和分類間的LINQ到SQL的ORM映射的關(guān)聯(lián)。在上面的表達(dá)式中,我也加了"order by c.CategoryName descending"子句來表示我要將結(jié)果集按名稱降序排列。
LINQ到SQL然后就會在使用這個表達(dá)式查詢數(shù)據(jù)庫時,生成下列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很聰明,只返回了我們所需的單個字段(分類名稱), 而且它是在數(shù)據(jù)庫層做了所有的過濾和排序,使得該查詢效率非常高。
查詢句法 - 用投影(Projection)來轉(zhuǎn)換數(shù)據(jù)
先前我指出的一個要點(diǎn)是,"select" 子句表示了你要返回的數(shù)據(jù),以及這個數(shù)據(jù)的構(gòu)形是什么。
譬如,假如你有個象下面這樣的"select p" 子句,這里p的類型是Person,然后,它就會返回一串Person對象:
LINQ和查詢句法提供的一個非常強(qiáng)大的功能是允許你定義跟被查詢的數(shù)據(jù)分開的新的類型,然后用新的類型來控制查詢返回的數(shù)據(jù)的形狀和結(jié)構(gòu)。
譬如,假設(shè)我們定義了一個新的AlternatePerson類,內(nèi)含一個FullName屬性,而不是我們原先的Person類內(nèi)的分開的FirstName和LastName屬性:
然后我就可以使用下面的LINQ查詢句法來查詢我原先的ListPerson>集合,用下面的查詢句法將結(jié)果轉(zhuǎn)換成一串AlternatePerson對象:
注意看,我們是如何在上面的表達(dá)式里的"select"子句里,使用我的語言系列的第一個貼子里討論過的新的對象初始化器句法來創(chuàng)建新的AlternatePerson實(shí)例,同時設(shè)置它的屬性的。也注意我是如何連接我們原先Person類的FirstName和LastName屬性,然后將其賦值給FullName屬性的。
對數(shù)據(jù)庫使用查詢句法投影
這個投影特性在操作從象數(shù)據(jù)庫這樣一個遠(yuǎn)程數(shù)據(jù)提供器那里取回的數(shù)據(jù)時,會變得難以置信地有用,因?yàn)樗峁┙o我們一個優(yōu)雅的方式,來表示我們的ORM應(yīng)該從數(shù)據(jù)庫實(shí)際取回哪些數(shù)據(jù)字段。
譬如,假設(shè)我用了LINQ到SQL的ORM提供器對Northwind數(shù)據(jù)庫建模,生成下面這些類:
通過編寫下面這個LINQ查詢,我告訴LINQ到SQL我要返回一串Product對象:
填充Product類所需的所有字段都將作為上面查詢的一部分從數(shù)據(jù)庫中返回,由LINQ到SQL orM執(zhí)行的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值):
然后我就可以使用下面這個查詢,使用查詢句法的投影功能來構(gòu)造我要從數(shù)據(jù)庫返回的數(shù)據(jù)的形狀:
這表明,不是返回一串Product對象,我要MyProduct對象,我只要其中三個屬性被賦值,LINQ到SQL就會很聰明地調(diào)整要執(zhí)行的raw SQL語句,從數(shù)據(jù)庫只返回那三個需要的產(chǎn)品字段:
Select [t0].[ProductID], [t0].[ProductName], [t0].[UnitPrice]
FROM [dbo].[Products] AS [t0]
Where [t0].[UnitPrice] > 99
為炫耀起見,我也可以填充MyProduct類的第四個屬性,即TotalRevenue屬性。我要這個值等于我們產(chǎn)品目前的銷售額的總量。這個值在Northwind數(shù)據(jù)庫中并沒有作為一個預(yù)先算好的字段而存在。而是,你需要在Products表和Order Details表間做一個關(guān)聯(lián),然后計(jì)算出一個給定產(chǎn)品對應(yīng)的所有的Order Detail 行的總量。
非??岬氖牵铱梢栽赑roduct類的OrderDetails關(guān)聯(lián)上使用LINQ的 Sum 這個擴(kuò)展方法,編寫一個作為我的查詢句法投影一部分的乘法Lambda表達(dá)式,來計(jì)算這個值:
LINQ到SQL就會非常聰明地使用下面這個SQL在SQL數(shù)據(jù)庫里做運(yùn)算:
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
查詢句法 - 理解延遲執(zhí)行(Deferred Execution)和使用ToList() 和ToArray()
在默認(rèn)情形下,查詢句法表達(dá)式的結(jié)果的類型是IEnumerableT>。在上面的例子里,你會注意到所有的查詢句法賦值是給IEnumerableProduct>, IEnumerablestring>, IEnumerablePerson>, IEnumerableAlternatePerson>, 和 IEnumerableMyProduct> 變量的。
IEnumerableT>接口的一個很好的特征是,實(shí)現(xiàn)它們的對象可以把實(shí)際的查詢運(yùn)算延遲到開發(fā)人員第一次試圖對返回值進(jìn)行迭代(這是通過使用最早在VS 2005中C# 2.0 中引進(jìn)的yield構(gòu)造來達(dá)成的)時才進(jìn)行。LINQ和查詢句法表達(dá)式利用了這個特性,將查詢的實(shí)際運(yùn)算延遲到了你第一次對返回值進(jìn)行循環(huán)時才進(jìn)行。假如你對IEnumerableT>的結(jié)果從不進(jìn)行迭代的話,那么查詢根本就不會執(zhí)行。
譬如,考慮下面這個LINQ到SQL的例子:
不是在查詢句法表達(dá)式聲明的時候,而是在我們第一次試圖對結(jié)果進(jìn)行循環(huán)(上面紅箭頭標(biāo)志的地方),才會去訪問數(shù)據(jù)庫以及取出填充Category對象所需的值。
這個延遲運(yùn)算的行為結(jié)果變得非常有用,因?yàn)樗俪闪艘恍┌讯鄠€LINQ查詢和表達(dá)式鏈在一起的強(qiáng)有力的組合場景。譬如,我們可以把一個表達(dá)式的結(jié)果喂給另一個表達(dá)式,然后通過延遲運(yùn)算,允許象LINQ 到SQL這樣的ORM根據(jù)整個表達(dá)式樹來優(yōu)化raw SQL。我將在以后的一個博客貼子里對這樣的場景做示范說明。
如何立刻對查詢句法表達(dá)式做運(yùn)算
如果你不要延遲查詢運(yùn)算,而是要對它們立刻就執(zhí)行運(yùn)算,你可以使用內(nèi)置的ToList() 和ToArray() 運(yùn)算符來返回一個包括了結(jié)果集的ListT>或者數(shù)組。
譬如,要返回一個基于范型的 ListT> 集合的話:
要返回一個數(shù)組的話:
在上面兩種情形下,會立刻訪問數(shù)據(jù)庫,填充Category對象。
結(jié)語
查詢句法在使用標(biāo)準(zhǔn)的LINQ查詢運(yùn)算符來表達(dá)查詢時,提供了非常方便的聲明式簡化寫法。它提供的句法可讀性非常高,可以針對任何類型的數(shù)據(jù)(內(nèi)存中的集合,數(shù)組,XML內(nèi)容,以及象數(shù)據(jù)庫這樣的遠(yuǎn)程數(shù)據(jù)提供器,web服務(wù)等等)進(jìn)行查詢。一旦你熟悉這個句法后,你可以在任何地方應(yīng)用這個知識。
在不遠(yuǎn)的將來,我將結(jié)束本語言系列的最后一部分,該部分將討論新的匿名類型特性。然后我將轉(zhuǎn)而討論在實(shí)際應(yīng)用中使用所有這些語言特性的一些非常實(shí)用的例子(特別是針對數(shù)據(jù)庫和XML文件使用LINQ的例子)。
希望本文對你有所幫助,
Scott