GroupJoin的應用
今天我們來看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前,先來喚醒大家對GroupBy的resultSelector的記憶,GroupBy的resultSelector會將每個鍵值及其資料傳入resultSelector,讓每個鍵值資料可以彙整回傳。
複習完GroupBy的resultSelector後,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
*/
- 用
Join及GroupBy實作
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的集合
因為GroupJoin的resultSelector可以拿到集合的資料,所以他可以做彙整的動作。
而Join只拿的到單筆資料,也就沒辦法做彙整了。
Left Join
之前介紹Join的時候有說過Join是Inner 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搭配DefaultIfEmpty和SelectMany達到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()叫用時才會執行 - 透過
SelectMany及DefaultIfEmpty可以對資料做Left Join
結語
GroupJoin的特性就像是Join跟GroupBy的合併,前半段作Join合併資料,後半段做GroupBy將相同鍵值的資料做彙整,下一章來看看GroupJoin是怎麼做到的。