連接Join
主要方法:
Join: 應用一個搜尋策略去匹配兩個集合中的元素, 並返回一個扁平的結果集, SQL對應語法為INNER JOIN
Group Join: 同上, 但返回的是一個層級的結果集, SQL對應語法為INNER JOIN, LEFT OUTER JOIN
概要
Join和GroupJoin將兩個輸入序列編織成一個單一的輸出序列, Join返回一個扁平的輸出結果集, GroupJoin則返回一個層級結果集.
Join和GroupJoin提供了Select與SelectMany之外的另一個選擇. Join和GroupJoin的優勢在於針對本地查詢更加高效,因為它首先將內部序列加載到一個lookup目錄當中,避免重復枚舉每一個內部元素. 它的劣勢在於只能對應於INNER JOIN和LEFT OUTER JOIN, 交叉連接和非等連接依然還是需要Select和SelectMany. 而對於LINQ to SQL, Join和GroupJoin對比Select和SelectMany並沒有提供任何額外的好處.
Join
Join操作符執行一個內連接(inner join), 輸出一個扁平序列
最簡單的演示Join用處的做法是使用LINQ to SQL, 以下的查詢列出所有的客戶以及他們的訂單信息而沒有使用關聯屬性
1: IQueryable<string> query = 2: 3: from c in dataContext.Customers 4: 5: join p in dataContext.Purchases 6: 7: on c.ID equals p.CustomerID 8: 9: select c.Name + ” bought a “ + p.Description;
結果與我們使用SelectMany查詢得到的結果一致
要了解Join相對於SelectMany額外的好處, 我們必須將它轉換為本地查詢, 以下的例子先將所有的客戶和采購訂單轉換為數組, 然後再做進一步的查詢:
1: Customer[] customers = dataContext.Customers.ToArray( ); 2: 3: Purchase[] purchases = dataContext.Purchases.ToArray( ); 4: 5: var slowQuery = 6: 7: from c in customers 8: 9: from p in purchases where c.ID == p.CustomerID 10: 11: select c.Name + ” bought a “ + p.Description; 12: 13: var fastQuery = 14: 15: from c in customers 16: 17: join p in purchases on c.ID equals p.CustomerID 18: 19: select c.Name + ” bought a “ + p.Description;
雖然兩種方式返回的結果集是一樣的, 但是Join查詢執行得更快一些, 因為它在Enumerable當中的實現預加載了內聯集合(purchases)到一個有鍵的字典中
Join執行一個內連接操作, 這意味著那些沒有采購訂單的客戶將被排除在輸出結果之外. 使用inner join, 你可以將inner和outer序列互換, 並且仍然可以得到同樣的結果:
1: from p in purchases 2: 3: join c in customers on p.CustomerID equals c.ID
我們可以增加更多的join語句到相同的查詢中, 例如, 假設每個采購訂單包含一或多個的采購明細, 我們可以像下面這樣將他們連接在一起:
1: from c in customers 2: 3: join p in purchases on c.ID equals p.CustomerID 4: 5: join pi in purchaseItems on p.ID equals pi.PurchaseID
Purchases在第一個連接中扮演了inner序列, 而在第二個連接中則扮演了outer序列的角色, 我們可以使用嵌套foreach得到相同的結果, 但是效率不高:
1: foreach (Customer c in customers) 2: 3: foreach (Purchase p in purchases) 4: 5: if (c.ID == p.CustomerID) 6: 7: foreach (PurchaseItem pi in purchaseItems) 8: 9: if (p.ID == pi.PurchaseID) 10: 11: Console.WriteLine (c.Name + “,” + p.Price + 12: 13: “,” + pi.Detail);
多主鍵連接
我們可以使用匿名類型來進行多主鍵鏈接操作:
1: from x in seqX 2: 3: join y in seqY on new { K1 = x.Prop1, K2 = x.Prop2 } 4: 5: equals new { K1 = y.Prop3, K2 = y.Prop4 }
為了能夠運行這個查詢, 兩個匿名類型的結構必須是相同的. 編譯器會將它們實現為相同的內部類型, 因此多主鍵鏈接能夠運行.
Lambda方式的連接
以下的示例使用了復合查詢語法:
1: from c in customers 2: 3: join p in purchases on c.ID equals p.CustomerID 4: 5: select new { c.Name, p.Description, p.Price };
使用Lambda表達式的話則可以改成這樣:
1: custGroupJoin
GroupJoin功能與Join類似, 只不過GroupJoin返回的結果集是一個層級結構, 而不是一個扁平的結構. 對於復合語法來說, GroupJoin和Join是一樣的, 只不過它通常會跟著一個into關鍵字:
1: var query = 2: 3: from c in customers 4: 5: join p in purchases on c.ID equals p.CustomerID 6: 7: into custPurchases 8: 9: select custPurchases;
只有當into語句出現在一個join語句之後它才會被翻譯成GroupJoin. 如果是跟在一個select或者group語句之後, 則意味著是查詢延續. 這兩個用法是非常不同的, 但它們有一個共同的特點: 兩者都引入了新的查詢變量
上述查詢結果是一個序列的序列, 我們可以像下面這樣來枚舉它:
1: foreach (IEnumerable purchaseSequence in query) 2: 3: foreach (Purchase p in purchaseSequence) 4: 5: Console.WriteLine (p.Description);
另外我們也可以在返回結果中來選取外部的查詢變量:
1: from c in customers 2: 3: join p in purchases on c.ID equals p.CustomerID 4: 5: into custPurchases 6: 7: select new { CustName = c.Name, custPurchases }; //c可以被引用到
使用下面的select子查詢也可以得到同樣的結果:
1: from c in customers 2: 3: select new 4: 5: { 6: 7: CustName = c.Name, 8: 9: custPurchases = 10: 11: purchases.Where (p => c.ID == p.CustomerID) 12: 13: };
默認情況下, GroupJoin相當於一個左外連接. 為了得到一個inner join, 我們必須在custPurcahse上面做一層過濾:
1: from c in customers join p in purchases 2: 3: on c.ID equals p.CustomerID 4: 5: into custPurchases 6: 7: where custPurchases.Any( ) 8: 9: select ...
在group-join之後的into操作作用於內部元素的子序列, 而不是每一個單獨的子元素, 因此如果你要過濾每一個單獨的采購單, 你必須在joining之前調用Where
1: from c in customers 2: 3: join p in purchases.Where (p2 => p2.Price > 1000) 4: 5: on c.ID equals p.CustomerID 6: 7: into custPurchases ...
扁平的Outer Joins
當你想得到一個Outer Join同時輸出扁平的結果集的時候, 這會是一個兩難的境地. GroupJoin會提供給你Outer Join; Join提供給你扁平的結果集. 因此, 解決方案是先調用GroupJoin, 然後在每一個子序列上面調用DefaultIfEmpty, 最後調用SelectMany:
1: from c in customers 2: 3: join p in purchases on c.ID equals p.CustomerID 4: 5: into custPurchases 6: 7: from cp in custPurchases.DefaultIfEmpty( ) 8: 9: select new 10: 11: { 12: 13: CustName = c.Name, 14: 15: Price = cp == null ? (decimal?) null : cp.Price 16: 17: };
當Purchases為空的時候DefaultIfEmpty會得到一個null值. 第二個from語句被翻譯成SelectMany, 它擴展和壓扁了所有的purchase, 並將它們聯合在一起形成purchase元素所屬的單一序列
omers.Join ( // outer collection 2: 3: purchases, // inner collection 4: 5: c => c.ID, // outer key selector 6: 7: p => p.CustomerID, // inner key selector 8: 9: (c, p) => new // result selector 10: 11: { c.Name, p.Description, p.Price } 12: 13: );
最後的結果選擇器表達式創建了輸出序列中的每一個元素, 如果你還有額外的查詢語句需要去執行, 例如orderby:
1: from c in customers 2: 3: join p in purchases on c.ID equals p.CustomerID 4: 5: orderby p.Price 6: 7: select c.Name + ” bought a “ + p.Description;
在Lambda方式中, 我們就必須在結果選擇器表達式中去生成一個臨時的匿名類型. 這樣可以保持c和p在同一個join作用范圍內:
1: customers.Join ( // outer collection 2: 3: purchases, // inner collection 4: 5: c => c.ID, // outer key selector 6: 7: p => p.CustomerID, // inner key selector 8: 9: (c, p) => new { c, p } ) // result selector 10: 11: .OrderBy (x => x.p.Price) 12: 13: .Select (x => x.c.Name + ” bought a “ 14: 15: + x.p.Description);
可以看得出來復合查詢語法更加的直觀一點, 這也是在使用joining操作時推薦的做法.