GroupJoin的應用

今天我們來看GroupByJoin的合體GroupJoin,一般資料表都會是一對多的關聯設計,很少會有一對一、多對多的情況出現,所以當我們Join完兩個資料時,我們得到的結果會是一邊的資料有重複的情形。

例如有個人有兩筆電話號碼,當我們Join人跟電話的資料時,這個人的資料就會出現兩筆,造成我們的資料處理上的困難,GroupJoin就是讓你在Join時就可以做彙整資料的作業,增加便利性。

功能說明

outer鍵值跟inner鍵值相等的資料合併,並且對資料進行彙整的動作。

方法定義

GroupJoinJoin一樣有兩個公開方法:

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的方法定義後,我們來跟Join比較,發現它們只差在resultSelector

而在講這個resultSelector前,先來喚醒大家對GroupByresultSelector的記憶,GroupByresultSelector會將每個鍵值及其資料傳入resultSelector,讓每個鍵值資料可以彙整回傳。

複習完GroupByresultSelector後,GroupJoin的也是跟其相似的,它是將outer對應的inner資料的集合跟著outer一起傳入resultSelector,這樣你就可以做彙整的動作。

查詢運算式

GroupJoin在查詢運算式中是跟Join用同樣的運算式: join,不同的是GroupJoin會在後面加一個into

join_into_clause
    : 'join' type? identifier 'in' expression 'on' expression 'equals' expression 'into' identifier
    ;

這個into的後面是接一個要在select中使用inner的別名,inner在查詢運算式中就是join後面定義的物件,現在我們來看一下轉換的公式:

下面這段查詢運算式:

from x1 in e1
join x2 in e2 on k1 equals k2 into g
select v

可以被轉換成GroupJoin:

( e1 ) . GroupJoin( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => v )

我們可以看到g是在inner Enumerable的位置,所以他會是x2中跟x1有關係的集合。

方法範例

這裡我們使用跟Join範例相同的物件及資料。

下面是電話的物件,電話上有個人的物件藉此跟人關聯:

class Person
{
    public string Name { get; set; }
}

class Phone
{
    public string PhoneNumber { get; set; }
    public Person Person { get; set; }
}

下面是範例資料:

Person Peter = new Person() { Name = "Peter" };
Person Sunny = new Person() { Name = "Sunny" };
Person Tim = new Person() { Name = "Tim" };
Person May = new Person() { Name = "May" };

Phone num1 = new Phone() { PhoneNumber = "01-5555555", Person = Peter };
Phone num2 = new Phone() { PhoneNumber = "02-5555555", Person = Sunny };
Phone num3 = new Phone() { PhoneNumber = "03-5555555", Person = Tim };
Phone num4 = new Phone() { PhoneNumber = "04-5555555", Person = May };
Phone num5 = new Phone() { PhoneNumber = "05-5555555", Person = Peter };

接下來我們來看幾個範例。

跟Join的比較

題目: 取得每個人的電話,如有多筆用逗號(,)隔開。

答案如下:

/*
 * output:
 *
 * Peter: 01-5555555,05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May: 04-5555555
 */
  1. JoinGroupBy實作
var results = persons.Join(
    phones,
    person => person,
    phone => phone.Person,
    (person, phone) => new { person.Name, phone.PhoneNumber })
    .GroupBy(x => x.Name,
        (name, data) => new {
            Name = name,
            PhoneNumber = string.Join(',', data.Select(x => x.PhoneNumber)) });
  1. GroupBy實作
var results = persons.GroupJoin(
    phones,
    person => person,
    phone => phone.Person,
    (person, phoneEnum) =>
        new {
            person.Name,
            PhoneNumber = string.Join(',', phoneEnum.Select(x => x.PhoneNumber))
        }
);

我們可以看到下面幾個重點:

  • 因為Join出來的資料是沒有分組的,所以需要再用GroupBy做分組
  • 兩個最大的差別在於resultSelector第二個傳入參數
    • Join是傳入此outer鍵值對應的其中一個inner資料
    • GroupJoin是傳入此outer鍵值對應的所有inner集合

因為GroupJoinresultSelector可以拿到集合的資料,所以他可以做彙整的動作。

Join只拿的到單筆資料,也就沒辦法做彙整了。

Left Join

之前介紹Join的時候有說過JoinInner Join,而GroupJoin可以經過一些手腳來取得Left Join的結果。

資料: 還是上一題的資料,為了可以看到Left Join的結果,我們把num4給拿掉,讓May沒有電話資料。

一般的Join會拿到Inner Join的資料:

var results = persons.Join(
    phones,
    person => person,
    phone => phone.Person,
    (person, phone) =>
        new
        {
            person.Name,
            phone.PhoneNumber
        }
);

/*
 * output
 * Peter: 01-5555555
 * Peter: 05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 */

GroupJoin搭配DefaultIfEmptySelectMany達到Left Join的效果

var results = persons.GroupJoin(
    phones,
    person => person,
    phone => phone.Person,
    (person, phoneEnum) => new
    {
        name = person.Name,
        phones = phoneEnum.DefaultIfEmpty()
    })
    .SelectMany(x => x.phones.Select(phone => new { name = x.name, phone = phone }))
    ;

/*
 * output
 * Peter: 01-5555555
 * Peter: 05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May:
 */
  • DefaultIfEmpty: 如果空的話回傳預設資料,讓此筆資料不會因為沒有電話資料而被刪掉
  • SelectMany: phones傳回來的是phone的集合,所以要用SelectMany把他打平

查詢運算式

題目: 使用查詢運算式取得資料。

  1. 一個人一筆資料(有做彙整)
var results = from person in persons
    join phone in phones on person equals phone.Person into ppGroup
    select new {person.Name, PhoneNumber= string.Join(',', ppGroup.Select(x => x.PhoneNumber))};

/*
 * output
 *
 * Peter: 01-5555555,05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May: 04-5555555
 */
  1. 每筆電話都一筆資料,沒有電話的人也要顯示名稱(Left Join)
var results = from person in persons
                join phone in phones on person equals phone.Person into ppGroup
                from item in ppGroup.DefaultIfEmpty(new Phone() { Person = null, PhoneNumber = ""})
                select new {name = person.Name, phone = item};

/*
 * output
 *
 * Peter: 01-5555555
 * Peter: 05-5555555
 * Sunny: 02-5555555
 * Tim: 03-5555555
 * May:
 */

特別之處

  • 延遲執行的特性,在foreach或是GetEnumerator()叫用時才會執行
  • 透過SelectManyDefaultIfEmpty可以對資料做Left Join

結語

GroupJoin的特性就像是JoinGroupBy的合併,前半段作Join合併資料,後半段做GroupBy將相同鍵值的資料做彙整,下一章來看看GroupJoin是怎麼做到的。

範例程式

GitHub

參考