iT邦幫忙

DAY 29
2

分享一些學習心得系列 第 29

LINQ自學筆記-語法應用-聚合資料-Join-3、GroupJoin

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 前,要先了解兩件事:

  1. 到目前為止,我們做的 Join 作業,精確來說,都是 Inner Join,也就是兩個來源序列中,都有相同的 TKey 項目才會出現在輸出序列中。
  2. LINQ 要做 Outer Join,必須先學會 GropuJoin 運算子。
    第一件事,應該很容易懂,第二件事則是一個新的運算子 GroupJoin。GroupJoin 運算子建議使用查詢表達式撰寫,方法架構查詢語法超難撰寫。它的功能 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 ,因為本文篇幅已經蠻長的,所以就留到下一篇再繼續分享,以免大家在學習時消化不良。


上一篇
LINQ自學筆記-語法應用-聚合資料-Join-2
下一篇
LINQ自學筆記-語法應用-聚合資料-DefaultIfEmpty 運算子、實做 Left Outer Join 效果
系列文
分享一些學習心得30

尚未有邦友留言

立即登入留言