解釋查詢(Interpreted QuerIEs)
LINQ提供了2個平行架構:針對本地對象集合的查詢(local query), 以及針對遠程數據源的查詢(interpreted query). 目前我們它討論的架構都是關於本地查詢的, 主要是操作那些實現了IEnumerable<>的對象集合.而Interpreted query則是用於操作那些實現了IQueryable<>的序列, Queryable類中的查詢操作符會通過執行運行時被解析的表達式樹得到最終結果.
(Enumerable類中的查詢操作符實際上也可以和IQueryable<>序列一起工作, 但問題是結果集只能在客戶機本地運行才能得到 — 也這是為什麼要在Queryable類中提供另外一套查詢操作符的原因.)
當前的.Net Framework提供了兩個IQueryable的具體實現:
1. LINQ to SQL
2. LINQ to EntitIEs
另外, 擴展方法AsQueryable可以針對任何可枚舉的集合生成一個IQueryable的包裝.
假定我們將使用以下的SQL腳本創建一個簡單的customer表並且插入一些名稱:
1: create table Customer 2: ( 3: ID int not null primary key, 4: Name varchar(30) 5: ) 6: insert Customer values (1, 'James') 7: insert Customer values (2, 'DanIEl') 8: insert Customer values (3, 'Carder') 9: insert Customer values (4, 'Amy') 10: insert Customer values (5, 'Joe')
這個表被創建以後, 我們可以編寫在C#代碼中編寫一個Interpreted的LINQ查詢用於查找那些名稱中包含a字符的記錄, 代碼如下:
1: using System; 2: using System.Linq; 3: using System.Data.Linq; 4: using System.Data.Linq.Mapping; 5: 6: [Table] public class Customer 7: { 8: [Column(IsPrimaryKey=true)] public int ID; 9: [Column] public string Name; 10: } 11: 12: class Test 13: { 14: static void Main( ) 15: { 16: var dataContext = new DataContext ("cx string... "); 17: Table<Customer> customers = dataContext.GetTable <Customer>( ); 18: 19: IQueryable<string> query = from c in customers 20: where c.Name.Contains ("a") 21: orderby c.Name.Length 22: select c.Name.ToUpper( ); 23: 24: foreach (string name in query) 25: Console.WriteLine (name); 26: } 27: }
LINQ to SQL將會把這個查詢翻譯成以下的SQL腳本:
1: SELECT UPPER([t0].[Name]) AS [value] 2: FROM [Customer] AS [t0] 3: WHERE [t0].[Name] LIKE '%a%' 4: ORDER BY LEN([t0].[Name])
並輸出結果:
AMY
JAMES
DANIEL
CARDER
Interpreted查詢是如何工作的?
讓我們查看一下前面提到的查詢是如何運行的.
首先, 編譯器將這個復合查詢轉換成Lambda表達式語法. 這個跟本地查詢的做法是一樣的:
IQueryable<string> q = customers
.Where (n=> n.Name.Contains (”a”))
.OrderBy (n => n.Name.Length)
.Select (n => n.Name.ToUpper());
接下來,編譯器將會解析查詢操作方法, 這裡就是本地查詢和Interpreted查詢的不同的地方了 – Interpreted查詢將會使用Queryable類型中的查詢方法而不是Enumerable類型中的.
要了解為什麼, 我們需要查看一下customers變量, 這是創建整個查詢的源頭. Customers是一個Table<>類型, 其實現了IQueryable<>(這同時又是一個IEnumerable<>的子類型). 這意味著編譯器在解析Where的時候有一個選擇: 可能調用Enumerable中的擴展方法, 或者Queryable中的擴展方法:
1: public static IQueryable<TSource> Where<TSource> ( 2: this IQueryable<TSource> source, 3: Expression <Func<TSource,bool>> predicate)
編譯器最終選擇Queryable.Where因為它的簽名更加明確的匹配.
注意, Queryable.Where接受一個在Expression類型中的predicate包裝, 這會指示編譯器翻譯Lambda表達式(n=>n.Name.Contains(“a”))到一個表達式樹而不是一個編譯的委托. 表達式樹是一個基於System.Linq.Expressions的對象模型, 其可以在運行時被檢視, 因此LINQ to SQL可以被翻譯成SQL表達式.
執行
Interpreted查詢遵循一個延遲執行模型,類似本地查詢,這意味著只有當你開始枚舉Query的時候SQL語句才會被生成. 並且, 枚舉相同的Query兩次將會對同樣對數據庫發起兩次查詢, 因此要小心避免引起性能問題.
Interpreted查詢不同於本地查詢的地方在於如何執行.當你開始枚舉一個Interpreted查詢的時候, 最外層的序列會運行一個程序橫貫整個表達式樹,將其處理成一個單元. 在我們的例子中,LINQ to SQL將表達式樹翻譯成SQL表達式, 然後運行並返回結果序列.
另外, 針對本地查詢,我們可以非常方便的使用迭代器編寫自己的擴展方法並將它們補充到之前定義的集合當中去. 但針對遠程查詢, 這是很困難的, 甚至是不合需要的. 如果你編寫了一個MyWhere的IQueryable<>擴展方法, 這就像你將一個你自己的產品(比如手機)放到一條生產筆記本的生產線上, 生產線上的機器根本無法處理. 即使你在這個階段進行干預, 你的解決方案你將與一個特殊的provider綁定在一起, (就像LINQ to SQL一樣), 並且不能和其他的IQueryable的實現一起工作. Queryable擁有的一系列方法的好處之一就是他們定義了操作遠程集合的標准詞匯-_-, 不過當你試圖擴展這些詞匯時, 他們就不再是可互操作的.
此模型引起的另外一個結果是一個IQueryable Provider可能無法處理某些查詢, 甚至是某些標准方法也是如此. 例如LINQ to SQL, 有些LINQ查詢是無法對應的SQL語法的, 這取決於目標數據庫服務器. 如果你對SQL非常熟悉, 你會對它們(那些可能無法翻譯的Query)有個直觀的感覺, 雖然通常情況下你可能需要進行一些實驗才能了解到底是什麼引起了運行時的錯誤, 那些翻譯後的SQL語句可能會令你感到驚訝. 使用最新版本的SQL Server進行工作是你的最佳選擇.
AsEnumerable
Enumerable.AsEnumerable是所有操作符中最簡單的一個, 它的完整定義是:
1: public static IEnumerable AsEnumerable 2: 3: (this IEnumerable source) 4: 5: { 6: 7: return source; 8: 9: }
它的主要目的是將IQueryable轉換成一個IEnumerable, 這將會強制將子序列綁定到Enumerable中的操作符而不是Queryable當中的.這意味著其後的執行都將會是在本地執行的.
舉個例子, 假設我們有一個Article表存在於SQL Server當中, 我們想使用LINQ to SQL列出所有Topic等於LINQ並且Content包含超過500個字符的文章:
1: Regex reg = new Regex (@”\b(\w|[-’])+\b”); 2: 3: var query = dataContext.Articles 4: 5: .Where (article => article.Topic ==“LINQ” && 6: 7: reg.Matches (article.Content).Count > 500);
這裡的問題是SQL Server並不支持正則表達式, 因此上面的Query將無法運行. 為了解決這個問題, 我們可以將其分成2步查詢:
1: Regex reg = new Regex (@”\b(\w|[’])+\b”); 2: 3: IEnumerable sqlQuery = 4: 5: dataContext.Articles 6: 7: .Where (a => a.Topic == “LINQ”); 8: 9: IEnumerable localQuery =sqlQuery 10: 11: .Where (a => reg.Matches (a.Content).Count > 500);
因為sqlQuery是一個IEnumerable類型, 其二次查詢將會被綁定到本地的操作符上面, 並在本地運行.使用AsEnumerable, 我們可以將上述的兩個查詢合並成一個:
1: Regex reg= new Regex (@”\b(\w|[-’])+\b”); 2: 3: var query = dataContext.Articles 4: 5: .Where (a => a.Topic == “LINQ”) 6: 7: .AsEnumerable() 8: 9: .Where (a => reg.Matches (a.Content).Count > 500);
除了AsEnumerable, 我們還可以使用ToArray或者ToList, 而AsEnumerable的好處就是延遲執行, 並且不會創建任何的存儲結構. 待續!