為什么說LINQ要勝過SQL
如果你還沒有沉溺于 LINQ,就會想這有啥大驚小怪的。SQL 并沒有壞掉,為什么還要對它進行修補呢? 為什么我們還需要另外一種查詢語言呢?
流行的說法是 LINQ 同 C#(或者 VB)集成在了一起,故而消除了編程語言和數(shù)據(jù)庫之間配合上的鴻溝,同時為多個數(shù)據(jù)源的組合提供了單一的查詢接口。雖然這些都是事實,但僅是故事的一部分。更重要的是:當要對數(shù)據(jù)庫進行查詢的時候,LINQ 在大多數(shù)情況下都比 SQL 更加有效。
同 SQL 相比, LINQ 更簡單、整潔而且高級。這樣子更像是拿 C# 同 C++ 做比較。真的,盡管有時候使用 C++ 仍然是最好的選擇(比如使用 SQL 的場景),但在大多數(shù)場景中,使用現(xiàn)代整潔的語言而不必為底層細節(jié)操作就是一項大勝利。
SQL 是一門非常古老的語言—發(fā)明于 1974 年。雖然經(jīng)歷過了無數(shù)此擴展,但從來沒有被重新設(shè)計過。這就使得它有點混亂了—不像是 VB6 或者 Visual FoxPro。你也許已經(jīng)慢慢變得習(xí)慣于此因而看不到任何錯漏的地方!
讓我們來看一個例子。你想要編寫一個簡單的查詢來獲取客戶數(shù)據(jù),如下:
- SELECT UPPER(Name)
- FROM Customer
- WHERE Name LIKE 'A%'
- ORDER BY Name
現(xiàn)在假設(shè)要將結(jié)果集里的這些數(shù)據(jù)提供給一個網(wǎng)頁,并且我們想獲取第 21 到 30 行數(shù)據(jù)。所以我們需要一個子查詢:
- SELECT UPPER(Name) FROM
- (
- SELECT *, RN = row_number()
- OVER (ORDER BY Name)
- FROM Customer
- WHERE Name LIKE 'A%'
- ) A
- WHERE RN BETWEEN 21 AND 30
- ORDER BY Name
而如果你需要支持版本(在 SQL Server 2005 之前的)更老的數(shù)據(jù)庫,情況會更糟糕:
- SELECT TOP 10 UPPER (c1.Name)
- FROM Customer c1
- WHERE
- c1.Name LIKE 'A%'
- AND c1.ID NOT IN
- (
- SELECT TOP 20 c2.ID
- FROM Customer c2
- WHERE c2.Name LIKE 'A%'
- ORDER BY c2.Name
- )
- ORDER BY c1.Name
這樣做不僅復(fù)雜而混亂,而且也違背了 DRY 原則。如下是使用 LINQ 實現(xiàn)相同的查詢功能。顯然在簡單性上更勝一籌:
- var query =
- from c in db.Customers
- where c.Name.StartsWith ("A")
- orderby c.Name
- select c.Name.ToUpper();
- var thirdPage = query.Skip(20).Take(10);
只有當我們枚舉到 thirdPage 時,查詢才會實際執(zhí)行。在從 LINQ 到 SQL 或者 Entity Framework 的場景中,翻譯引擎會將(我們用兩個步驟組合而成的)查詢轉(zhuǎn)換成一個 SQL 語句,這個語句是針對其所連接的數(shù)據(jù)庫服務(wù)器進行了優(yōu)化的。
可組合性
您可能已經(jīng)注意到 LINQ 的另一個更微妙(微妙但意義重大)的好處。我們選擇了組合中的兩個查詢步驟:
- IQueryable Paginate (this IQueryable query, int skip, int take)
- {
- return query.Skip(skip).Take(take);
- }
我們可以這樣做:
- var query = ...
- var thirdPage = query.Paginate (20, 10);
更重要的是,在這里我們可以進行任意的分頁查詢。換言之就是通過 LINQ 你可以把查詢分解成一部分,然后在你的應(yīng)用程序中重用。
聯(lián)合
LINQ 另一好處就是你可以不用 JOIN 就能進行關(guān)系間查詢。例如,我們想要列出所有購物在 $1000 或者以上,并且居住在華盛頓的顧客。我們會假定讓購買項目化(也就是經(jīng)典的采購/項目采購場景)并且把(沒有顧客記錄的)現(xiàn)金銷售也囊括進來。這就需要在四個表(Purchase, Customer, Address 以及 PurchaseItem)之間進行查詢。使用 LINQ,這樣的查詢不費吹灰之力:
- from p in db.Purchases
- where p.Customer.Address.State == "WA" || p.Customer == null
- where p.PurchaseItems.Sum (pi => pi.SaleAmount) > 1000
- select p
將此與同等功能的 SQL 相比較:
- SELECT p.*
- FROM Purchase p
- LEFT OUTER JOIN
- Customer c INNER JOIN Address a ON c.AddressID = a.ID
- ON p.CustomerID = c.ID
- WHERE
- (a.State = 'WA' || p.CustomerID IS NULL)
- AND p.ID in
- (
- SELECT PurchaseID FROM PurchaseItem
- GROUP BY PurchaseID HAVING SUM (SaleAmount) > 1000
- )
對此例進一步擴展,假設(shè)我們想要將結(jié)果集按價格進行逆序排列,并在最終的投影中顯示銷售員的姓名以及所購買項目的數(shù)量。我們可以自然不重復(fù)地表達出這些附件的查詢條件:
- from p in db.Purchases
- where p.Customer.Address.State == "WA" || p.Customer == null
- let purchaseValue = p.PurchaseItems.Sum (pi => pi.SaleAmount)
- where purchaseValue > 1000
- orderby purchaseValue descending
- select new
- {
- p.Description,
- p.Customer.SalesPerson.Name,
- PurchaseItemCount = p.PurchaseItems.Count()
- }
下面是使用 SQL 實現(xiàn)相同的查詢:
- SELECT
- p.Description,
- s.Name,
- (SELECT COUNT(*) FROM PurchaseItem pi WHERE p.ID = pi.PurchaseID) PurchaseItemCount
- FROM Purchase p
- LEFT OUTER JOIN
- Customer c
- INNER JOIN Address a ON c.AddressID = a.ID
- LEFT OUTER JOIN SalesPerson s ON c.SalesPersonID = s.ID
- ON p.CustomerID = c.ID
- WHERE
- (a.State = 'WA' OR p.CustomerID IS NULL)
- AND p.ID in
- (
- SELECT PurchaseID FROM PurchaseItem
- GROUP BY PurchaseID HAVING SUM (SaleAmount) > 1000
- )
- ORDER BY
- (SELECT SUM (SaleAmount) FROM PurchaseItem pi WHERE p.ID = pi.PurchaseID) DESC
有意思的是可以將上述 SQL 查詢轉(zhuǎn)換回到 LINQ,所生成的查詢每一塊都會有傻瓜式重復(fù)。論壇里常會貼出這樣的查詢(通常是非工作的版本)——這是用 SQL 進行思考而不是以 LINQ 進行思考的結(jié)果。這就像是是將 Fortran 程序轉(zhuǎn)換成 C# 6 時會抱怨 GOTO 的笨拙語法一樣。
數(shù)據(jù)修整
在查詢聯(lián)合中從多個表選擇數(shù)據(jù) – 最終的結(jié)果會是一個扁平的以行為單位的元組。如果你使用了多年的 SQL,你可能認為這種事不會發(fā)生在你身上——它導(dǎo)致數(shù)據(jù)重復(fù),從而使得結(jié)果集無法在客戶端很好地使用。所以當它發(fā)生時往往難以接受。與此相反,LINQ 讓你可以獲取到休整過的分層級的數(shù)據(jù)。這就避免了重復(fù),讓結(jié)果集容易處理,而且在大多數(shù)情況下也會消除進行聯(lián)合操作的必要。例如,假設(shè)我們想要提取一組顧客,每一條記錄都帶上了它們的高價值交易。使用 LINQ,你可以這樣做:
- from c in db.Customers
- where c.Address.State == "WA"
- select new
- {
- c.Name,
- c.CustomerNumber,
- HighValuePurchases = c.Purchases.Where (p => p.Price > 1000)
- }
HighValuePurchases,在這里是一個集合。由于我們查詢的是一個相關(guān)屬性,就不需要進行聯(lián)合了。因此這是一個內(nèi)聯(lián)合還是外聯(lián)合的細節(jié)問題就被很好的抽象掉了。在此例中,當翻譯成了 SQL,可能就是一個外聯(lián)合:LINQ 不會因為子集合返回的是零個元素就排除行。如果我們想要有一個可以翻譯成一個內(nèi)聯(lián)合的東西,可以這樣做:
- from c in db.Customers
- where c.Address.State == "WA"
- let HighValuePurchases = c.Purchases.Where (p => p.Price > 1000)where HighValuePurchases.Any()select new
- {
- c.Name,
- c.CustomerNumber,
- HighValuePurchases
- }
LINQ 還通過一組豐富的操作符對平面外聯(lián)合、自聯(lián)合、組查詢以及其它各種不同類型查詢進行了支持。
參數(shù)化
如果我們想要將之前的例子參數(shù)化會如何呢,如此”WA”狀態(tài)是不是就要來自于一個變量呢? 其實我們只要像下面這樣做就可以了:
- string state = "WA";
- var query =
- from c in db.Customers
- where c.Address.State == state
- ...
不會混淆 DbCommand 對象上面的參數(shù),或者擔(dān)心 SQL 注入攻擊。 LINQ 的參數(shù)化是內(nèi)聯(lián)、類型安全并且高度可讀的。它不僅解決了問題——而且解決得很不錯。
因為 LINQ 查詢時可以進行組合,所以我們可以有條件的添加謂詞。例如,我們寫出一個方法,如下:
- IQueryable GetCustomers (string state, decimal? minPurchase)
- {
- var query = Customers.AsQueryable();
- if (state != null)
- query = query.Where (c => c.Address.State == state);
- if (minPurchase != null)
- query = query.Where (c => c.Purchases.Any (p => p.Price > minPurchase.Value));
- return query;
- }
如果我們使用空的 state 以及 minPurchase 值調(diào)用了這個方法,那么在我們枚舉結(jié)果集的時候如下 SQL 就會被生成出來:
- SELECT [t0].[ID], [t0].[Name], [t0].[AddressID]
- FROM [Customer] AS [t0]
不過,如果我們指定了 state 和 minPurchase 的值,LINQ 到 SQL 就不只是向查詢添加了謂詞,還會有必要的聯(lián)合語句:
- SELECT [t0].[ID], [t0].[Name], [t0].[AddressID]
- FROM [Customer] AS [t0]
- LEFT OUTER JOIN [Address] AS [t1] ON [t1].[ID] = [t0].[AddressID]
- WHERE (EXISTS(
- SELECT NULL AS [EMPTY]
- FROM [Purchase] AS [t2]
- WHERE ([t2].[Price] > @p0) AND ([t2].[CustomerID] = [t0].[ID])
- )) AND ([t1].[State] = @p1)
因為我們的方法返回了一個 IQueryable,查詢在枚舉到之前并不會被實際地轉(zhuǎn)換成 SQL 并加以執(zhí)行。這樣就給了調(diào)用進一步添加謂詞、分頁、自定義投影等等的機會。
靜態(tài)類型安全
在之前的查詢中,如果我們將 state 變量聲明成了一個整型數(shù)而不是一個字符串,那么查詢可能在編譯時就會報錯,而不用等到運行時。這個也同樣適用于把表名或者列名弄錯的情況。這在重構(gòu)時有一個很實在的好處:如果你沒有完成手頭的工作,編譯器會給出提示。
客戶端處理
LINQ 讓你可以輕松地將查詢的一些部分轉(zhuǎn)移到客戶端上進行處理。對于負載負擔(dān)較大的數(shù)據(jù)庫服務(wù)器,這樣做可實際提升性能。只要你所取數(shù)據(jù)沒有超過所需(換言之,你還是要在服務(wù)器上做過濾),就可以經(jīng)常性地通過把對結(jié)果集進行重新排序、轉(zhuǎn)換以及重組的壓力轉(zhuǎn)移到負載較少的應(yīng)用服務(wù)器上去。使用 LINQ,你需要做的就是 AsEnumerable() 轉(zhuǎn)移到查詢之中,而自那個點之后的所有事情都可以在本地執(zhí)行。
什么時候不用 LINQ 去查詢數(shù)據(jù)庫
盡管 LINQ 的功能強大,但是它并不能取代 SQL。它可以滿足 95% 以上的需求,不過你有時仍然需要SQL:
- 需要手動調(diào)整的查詢 (特殊是需要優(yōu)化和進行鎖定提示的時候);
- 有些涉及到要 select 臨時表,然后又要對那些表進行查詢操作的查詢;
- 預(yù)知的更新以及批量插入操作。
還有就在用到觸發(fā)器時,你還是需要 SQL。 (盡管在使用 LINQ 的時候諸如此類的東西并非常常被需要,但在要使用存儲過程和函數(shù)的時候,SQL 是不可或缺的)。你可以通過在 SQL 中編寫表值函數(shù)來將 SQL 與 LINQ 結(jié)合在一起, 然后在更加復(fù)雜的 LINQ 查詢里面調(diào)用這些函數(shù)。
了解兩門查詢語言并不是問題,因為無論如何你都會想要去學(xué)習(xí) LINQ 的 — LINQ 在查詢本地集合以及 XML DOM 的時候非常實用。如果你使用的仍然是老舊的基于 XmlDocument 的 DOM,LINQ to XML 的 DOM 操作會是一種具有戲劇效果的進步。
還有就是相比于 SQL, LINQ 更易于掌握,所以如果你想寫個不錯的查詢,使用 LINQ 會比 SQL 更好達成。
將 LINQ 用于實戰(zhàn)
我?guī)缀跏侵挥?LINQ 來做數(shù)據(jù)庫查詢,因為它更有效率。
對于應(yīng)用程序的編寫而言,我的個人經(jīng)驗是一個使用 LINQ 的數(shù)據(jù)訪問層(使用一個像 LINQ 到 SQL 或者 Entity Framework 的 API)可以將數(shù)據(jù)訪問的開發(fā)時間砍掉一半,而且可以讓維護工作更加的輕松。