今天我們來看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 };
接下來我們來看幾個範例。
題目: 取得每個人的電話,如有多筆用逗號(,
)隔開。
答案如下:
/*
* 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
只拿的到單筆資料,也就沒辦法做彙整了。
之前介紹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
是怎麼做到的。