iT邦幫忙

DAY 30
2

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

LINQ自學筆記-語法應用-聚合資料-DefaultIfEmpty 運算子、實做 Left Outer Join 效果

Join 運算子的第四篇文章,將和大家分享如何利用 GroupJoin 運算子完成 Left outer join 的效果。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
上一篇我們說明了 GroupJoin 運算子,接下我們要利用這個運算子來完成仿 SQL Script 的 Left outer join (左外部聯結)效果。But,人生最機車的就是這個 But,在看 Code 之前,我們要再多了解一個運算子:DefaultIfEmpty。

public static IEnumerable<TSource> DefaultIfEmpty<TSource>(
    this IEnumerable<TSource> source
)
public static IEnumerable<TSource> DefaultIfEmpty<TSource>(
    this IEnumerable<TSource> source,
    TSource defaultValue
)

DefaultIfEmpty 運算子是 IEnumerable<TSource> 的擴充方法,呼叫它時,會回傳 IEnumerable<TSource>,來源和結果看起來都一樣,唯一的差異是 DefaultIfEmpty 運算子所回傳的序列,其中若有項目是 null,則會以該項目型別(TSource)的預設值帶入,或者我們可以利用第二個多載方法自訂項目為 null 時的預設值。

我們應該了解,外部聯結(Outer join)的意義就是,只要 outer 有的項目,不管在 inner 中是否能對應到,都必須呈現在輸出結果中,換言之,outer 中的項目,對應到在 inner 中有可能是 null,但是 null 是無法變成輸出結果,會發生 NullReferenceException 錯誤,因此我們必須透過 DefaultIfEmpty 運算子幫我們處理 TInner 為 null 時,該怎麼定義 TInner 的輸出結果。

大家可能會問,那有沒有 Right outer join?在 LINQ 裡,沒有!請使用 Left outer join 吧。

But,是的,人生最機車的 But 又出現了!.Net 3.5 的 LINQ to SQL/Entities 完全不支援 DefaultIfEmpty 運算子,.Net 4.0/4.5 的 LINQ to SQL 只支援第一個無參數的多載方法,至於 LINQ to Entities ,MSDN 文件上是寫兩個多載方法都支援,不過實際在 .Net 4.0 上,使用第二個多載方法還是會出現 NotSupportedException 例外,原因我還沒查出來,有結果再回補文章,總之,使用時請留意,才不會老是發生 NotSupportedException 錯誤。

下面範例我們沿用上一篇的 BOM 和 Equip 資料來源,因此程式碼會省略資料來源的定義,請自行翻閱前一篇文章之程式碼,以建立資料來源:

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 } into be
        from obj in be.DefaultIfEmpty()
        select new 
        {
            b.BomType, b.WUC,
            Name = obj == null ? "N/A" : obj.Name
        };
    query.Dump("Left outer join query");
}


上述程式碼,我們使用 GroupJoin 運算子聯結 BOMTable 和 Equips 兩個序列,設定用 BomType 和 WUC 屬性做相等比較,到目前為止都還是內部聯結(Inner join),接著我們對 GroupJoin 後的結果序列(be)調用 DefaultIfEmpty 運算子,使用無參數的第一個多載方法,然後在投影(Select)輸出建立匿名型別的 Name 屬性時,判斷若 be 中的項目為 null,就輸出字串 "N/A",否則就輸出項目的 Name 屬性值,至此,完成 Left outer join 效果。

我們看一下對等的方法架構查詢:

BOMTable
   .GroupJoin (
      Equips, 
      b => 
         new  
         {
            BomType = b.BomType, 
            WUC = b.WUC
         }, 
      e => 
         new  
         {
            BomType = e.BomType, 
            WUC = e.WUC
         }, 
      (b, be) => 
         new  
         {
            b = b, 
            be = be
         }
   )
   .SelectMany (
      temp0 => temp0.be.DefaultIfEmpty (), 
      (temp0, obj) => 
         new  
         {
            BomType = temp0.b.BomType, 
            WUC = temp0.b.WUC, 
            Name = (obj == null) ? "N/A" : obj.Name
         }
   )

如果改成查詢資料庫,則會產出的 SQL Script 如下:

SELECT 
[Extent1].[BomType] AS [BomType], 
[Extent1].[WUC] AS [WUC], 
CASE WHEN ([Extent2].[Name] IS NULL) THEN N'N/A' ELSE [Extent2].[Name] END AS [C1]
FROM  [dbo].[BOMTable] AS [Extent1]
LEFT OUTER JOIN [dbo].[Equip] AS [Extent2] ON ([Extent1].[BomType] = [Extent2].[BomType]) AND ([Extent1].[WUC] = [Extent2].[WUC])

看到囉,產出的 SQL Script 中,有 Left outer join 喔!
接著,看幾個第二個多載方法的範例,一樣可以完成 Left outer join 效果:

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 } into be
        from obj in be.DefaultIfEmpty(new Equip {Name = default(string)})
        select new 
        {
            b.BomType, b.WUC,
            Name = obj.Name ?? "N/A"
        };
    query.Dump("Left outer join query");
}

輸出結果和前一個範例是一樣的,就不重複貼圖佔版面了。上述程式碼,改用第二個多載方法,呼叫 DefaultIfEmply 運算子帶入一個新的 Equip 型別,只設定 Name 屬性的預設值,然後投影輸出時,改用 ?? 運算子(Null 結合)處理輸出的 Name 屬性值。

對應的方法架構查詢:

BOMTable
   .GroupJoin (
      Equips, 
      b => 
         new  
         {
            BomType = b.BomType, 
            WUC = b.WUC
         }, 
      e => 
         new  
         {
            BomType = e.BomType, 
            WUC = e.WUC
         }, 
      (b, be) => 
         new  
         {
            b = b, 
            be = be
         }
   )
   .SelectMany (
      temp0 => 
         temp0.be
            .DefaultIfEmpty (
               new Equip()
               {
                  Name = null
               }
            ), 
      (temp0, obj) => 
         new  
         {
            BomType = temp0.b.BomType, 
            WUC = temp0.b.WUC, 
            Name = (obj.Name ?? "N/A")
         }
   ) 

第三種應用 DefaultIfEmpty 運算子的方式,是在投影輸出時使用,請見下述範例:

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 } into be
        select be.DefaultIfEmpty(new Equip
        {
            BomType = b.BomType, 
            WUC = b.WUC,
            Name = "N/A"
        });
    query.SelectMany(q => q).Dump("Left outer join query");
}

輸出結果亦同前面的範例。上述程式碼,把 DefaultIfEmpty 運算子放到投影時輸出,但是因為它會回傳一個序列,所以輸出結果其實是一個集合再包一個集合,所以最後我用 SelectMany 做扁平化處理,讓輸出結果和前面的範例一致。


上一篇
LINQ自學筆記-語法應用-聚合資料-Join-3、GroupJoin
系列文
分享一些學習心得30

2 則留言

0
SunAllen
iT邦研究生 1 級 ‧ 2012-10-25 21:42:06

恭喜smartleos鐵人賽達陣灑花灑花灑花

0
smartleos
iT邦新手 3 級 ‧ 2012-10-31 14:13:09

謝謝啦 ^^
不好意思現在才回覆,因為鐵人賽達陣後,我就徹底休息到現在啦。接下要繼續把未完的主題寫完 哈哈

我要留言

立即登入留言