Join 運算子的第三篇文章,將和大家分享多主鍵聯結和 GroupJoin 運算子。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
Join 運算子,關鍵是要用來源序列中的什麼欄位來做相等比較!前面兩篇文章的範例都是單純使用一個欄位來比較,但實務上,若是 LINQ to SQL/Entities,複合主鍵的情況很常見,因此本文第一個重點就要介紹如何使用複合主鍵的 Join 運算子。
本篇文章要重新設計資料來源,且這個資料來源也會應用在下一篇,所以第一個範例會先貼上完整的資料來源程式碼。
新的資料來源,分別是 BOM 和 Equip 兩個類別。BOM 類別有兩個屬性,用以設定類型(BomType)和可安裝的工作單位碼(WUC);Equip 類別有三個屬性,除了名稱(Name)外,另外兩個欄位就是 BomType 和 WUC,表示此物料可以安裝在 BOM 表的什麼槽位。下面的程式碼就利用 BomType 和 WUC 兩個屬性做多主鍵聯結:
void Main()
{
var BOMTable = DataProvider.getBOM();
var Equips = DataProvider.getEquips();
var query =
from b in BOMTable
join e in Equips
on new { b.BomType, b.WUC } equals new { e.BomType, e.WUC }
select new
{
b.BomType,
b.WUC,
e.Name
};
query.Dump ("Join query");
}
public static class DataProvider
{
public static List<BOM> getBOM()
{
var bom = new List<BOM>
{
new BOM {BomType = 111, WUC = "Common"},
new BOM {BomType = 111, WUC = "Tester"},
new BOM {BomType = 111, WUC = "Normal"},
new BOM {BomType = 777, WUC = "Normal"},
new BOM {BomType = 777, WUC = "Tester"},
new BOM {BomType = 999, WUC = "Common"},
new BOM {BomType = 999, WUC = "Once"},
new BOM {BomType = 999, WUC = "Normal"},
new BOM {BomType = 000, WUC = "Reserve"}
};
return bom;
}
public static List<Equip> getEquips()
{
var Equips = new List<Equip>
{
new Equip {Name = "Adjustable Race", BomType = 999, WUC = "Common"},
new Equip {Name = "Headset Ball Bearings", BomType = 999, WUC = "Normal"},
new Equip {Name = "Freewheel", BomType = 999, WUC = "Normal"},
new Equip {Name = "Front Derailleur Cage", BomType = 999, WUC = "Once"},
new Equip {Name = "LL Crankarm", BomType = 999, WUC = "Common"},
new Equip {Name = "ML Crankarm", BomType = 999, WUC = "Common"},
new Equip {Name = "HL Crankarm", BomType = 111, WUC = "Common"},
new Equip {Name = "Touring End Caps", BomType = 111, WUC = "Tester"},
new Equip {Name = "Keyed Washer", BomType = 111, WUC = "Tester"},
new Equip {Name = "Hex Nut 3", BomType = 111, WUC = "Common"},
new Equip {Name = "Hex Nut 5", BomType = 777, WUC = "Normal"},
new Equip {Name = "Hex Nut 7", BomType = 777, WUC = "Normal"},
new Equip {Name = "LL Hub", BomType = 777, WUC = "Tester"},
new Equip {Name = "ML Hub", BomType = 777, WUC = "Tester"},
new Equip {Name = "XL Hub", BomType = 777, WUC = "Tester"},
new Equip {Name = "Fork Crown", BomType = 777, WUC = "Tester"}
};
return Equips;
}
}
public class BOM
{
public int ID {get; set; }
public int BomType { get; set; }
public string WUC { get; set; }
}
public class Equip
{
public string Name { get; set; }
public int BomType { get; set; }
public string WUC { get; set; }
}
上述程式碼透過 BomType 和 WUC 屬性,將 BOM 和 Equip 做聯結,呈現出目前 BOM 上各個槽位(BomType + WUC)上可用的設備清單。
PS. BOM = Bill of Materials,物料清單,通常用在倉儲管理、物料管理相關的系統中。
但上述結果我們並不滿意,因為當要呈現整份物料清單的現況時,我們會希望把所有空槽位也列出來,也就是 SQL Script 的 Outer Join 效果。那這個效果在 LINQ 中該怎麼做?
講解 Outer Join 前,要先了解兩件事:
Join 運算子,只要 TOuter 對應到一個 TInner,都會把 TOuter 和 TInner 丟給 resultSelector 變成輸出序列中的一筆資料,所以若 TOuter 對應到多個 TInner,則每次都會呼叫 resultSelector 一次,因此這種情況發生時,在輸出序列中,TOuter 會有多筆資料,但是 GroupJoin 運算子在輸出的處理上就不同了,我們先看它兩個多載方法簽名碼:
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector
)
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector,
IEqualityComparer<TKey> comparer
)
注意 GroupJoin 的 resultSelector 委派,傳入的是 TOuter 和 IEnumerable<TInner>,也就是說,每次拿 TOuter 比對 TInner 時,GroupJoin 運算子會把所有 TKey 符合的 TInner 都收集到 IEnumerable<TInner>,最後一次丟給 resultSelector 處理,變成輸出序列中的一筆【集合】資料。
我們看下述範例,注意,這個範例還是延用前兩篇文章的 Customers、Orders 資料來源,方便說明 GroupJoin 效果:
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
var groupJoinQuery = from c in Customers
join o in Orders on c.ID equals o.CustomerID
into custOrders
select custOrders;
groupJoinQuery.Dump();
var joinQuery = from c in Customers
join o in Orders on c.ID equals o.CustomerID
select o;
joinQuery.Dump();
}
上述程式碼定義兩個查詢,第一個是 GroupJoin,第二個查詢是 Join,可明顯從輸出結果看到,GroupJoin 運算子輸出的是 EnumerableQuery<IEnumerable<Order>>,但是 Join 運算子輸出的是 IEnumerable<Order>。
請注意,GroupJoin 運算子,通常都不是像範例中這樣簡單的應用,因為上述方式,其實不使用 GroupJoin 也可以啊!所以其實 GroupJoin 運算子,主要是讓我們針對聯結後的結果做進一步的過濾、查詢使用,像是要完成 Outer Join 的效果就必須用 GroupJoin 的方式。
以下是上述 GroupJoin 運算子的查詢表達式之對應方法架構查詢:
Customers
.GroupJoin (
Orders,
c => c.ID,
o => o.CustomerID,
(c, custOrders) => custOrders
)
看起來似乎很簡單,和 Join 差不多對吧,但是想想,若是多主鍵聯結的查詢呢?
from b in BOMTable.AsQueryable()
join e in Equips
on new { b.BomType, b.WUC } equals new { e.BomType, e.WUC } into be
select be;
//對應的方法架構查詢
BOMTable
.GroupJoin (
Equips,
b =>
new
{
BomType = b.BomType,
WUC = b.WUC
},
e =>
new
{
BomType = e.BomType,
WUC = e.WUC
},
(b, be) => be
)
相對查詢表達式,複雜多了吧!所以強烈建立,還是用查詢表達式寫 Join、GroupJoin 會輕鬆很多。有簡便的方式,何必自找麻煩是喔!
關於 Outer Join ,因為本文篇幅已經蠻長的,所以就留到下一篇再繼續分享,以免大家在學習時消化不良。