添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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

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

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

GroupJoin Join 一樣有兩個公開方法:

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
  • 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)) });
    
  • 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) =>
                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把他打平

    查詢運算式

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

  • 一個人一筆資料(有做彙整)
  • 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
    
  • 每筆電話都一筆資料,沒有電話的人也要顯示名稱(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

    docs.microsoft: system.linq.enumerable.groupjoin docs.microsoft: query-expressions docs.microsoft: join-clause stackoverflow: how-to-implement-left-join-in-join-extension-method
  •