1. はじめに
たまに、共通の特徴でつながってる2つの違うコレクションを扱うことがあるよね。例えば、注文リストと別の顧客コレクションがあるとする。どの注文がどの顧客のものか、どうやって判断する?そのためには、両方のコレクションが共通のID、例えばuserIdでつながってる必要があるんだ。
リレーショナルデータベースだと、こういうのはJOIN操作で解決する。これは違うテーブルの行を一致するキーで結合するやつ。LINQにも同じようなオペレーターがあって、それもJoinって名前なんだ。
2つのテーブルを想像してみて:1つ目は図書館の読者リスト(ID付き)、2つ目は本の注文リストで、各注文にid(読者のID)が付いてる。joinを使えば、この2つのテーブルをidで「くっつけて」、結果として「読者+その注文」のペアができるってわけ。
ちなみに、「joinってデータベース専用でしょ?」って思ってたら、かなり損してるよ!プログラミングではコレクションを結合することがよくあるし、特に外部システムや構造化ファイル(JSON、XML、経理からもらったExcelの表とか)を扱う時はなおさら。
2. Joinメソッドのシグネチャと仕組み
Joinメソッドはこんな感じ:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // 最初のコレクション(外側)
IEnumerable<TInner> inner, // 2つ目のコレクション(内側)
Func<TOuter, TKey> outerKeySelector, // 外側コレクションからキーを取得する方法
Func<TInner, TKey> innerKeySelector, // 内側からキーを取得する方法
Func<TOuter, TInner, TResult> resultSelector) // 結果を生成する関数(新しい要素)
最初はちょっとゴチャゴチャしてるように見えるけど、今からちゃんと分解して説明するね。
どう動くか:
最初のコレクション(outer)の各要素について、LINQは2つ目(inner)からキーが一致する要素を探す。キーが一致したらresultSelectorが呼ばれて、結果が最終コレクションに追加されるって流れ。
3. 例:顧客とその注文を結合する
クラスとコレクションをいくつか作ってみよう。このアイデアは、僕らの学習用アプリの続きって感じ(コースの途中で作ってる小さなお店アプリだと思って)。
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string Product { get; set; } = "";
}
// 顧客コレクション
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "バシャ" },
new Customer { Id = 2, Name = "ペチャ" },
new Customer { Id = 3, Name = "マーシャ" },
};
// 注文コレクション
var orders = new List<Order>
{
new Order { Id = 101, CustomerId = 2, Product = "本" },
new Order { Id = 102, CustomerId = 1, Product = "ペン" },
new Order { Id = 103, CustomerId = 2, Product = "ノート" },
new Order { Id = 104, CustomerId = 3, Product = "消しゴム" },
};
今度は、全ての顧客とその注文を取得したい。例えば、「ペチャが本を注文した」「ペチャがノートを注文した」みたいに出力したいんだ。
Joinを使ってみる
// customer.Idとorder.CustomerIdで顧客と注文を結合
var query = customers.Join(
orders,
customer => customer.Id, // 顧客からキーを取得
order => order.CustomerId, // 注文からキーを取得
(customer, order) => new // 一致したペアで新しいオブジェクトを作る
{
CustomerName = customer.Name,
Product = order.Product
}
);
// 結果を出力
foreach (var item in query)
{
Console.WriteLine($"{item.CustomerName}が{item.Product}を注文した");
}
出力される内容:
バシャがペンを注文した
ペチャが本を注文した
ペチャがノートを注文した
マーシャが消しゴムを注文した
ポイント:1人の顧客が複数の注文を持ってる場合、注文ごとにリストに何回も出てくる。これが正しい動作だよ!
4. テーブル:JoinとGroupBy + SelectManyの比較
| 操作 | 結果 | 使いどころ |
|---|---|---|
|
フラットなペアのリスト | クラシックなSQL JOIN。各ペア(一致)は1行。 |
|
サブコレクション付きのグループ | 「顧客+その全注文」みたいな「1対多」構造が欲しい時。 |
初心者は「顧客→注文リスト」みたいな構造が欲しい時にもJoinを使いがち。でもJoinは1行ずつ処理してグループ化しない。そういう時はGroupJoin(次のレクチャーでやるよ)かGroupByを使おう。
5. Query Syntax(SQL風LINQ)でのJoin
LINQには2つの書き方がある:メソッドチェーン(Method Syntax、例:Join(...))と、いわゆるSQL風(Query Syntax)で、見た目が普通のSQLクエリっぽい。
特にデータベース経験者には、この書き方の方が分かりやすいこともある:
var query2 =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
Product = order.Product
};
foreach (var item in query2)
{
Console.WriteLine($"{item.CustomerName}が{item.Product}を注文した");
}
注意:
Query Syntaxではequalsキーワードを使う。==オペレーターはここでは使えないよ!
これ、初心者の面接でよく引っかかるポイントだから気をつけて😉
6. 大事なポイントと落とし穴
たまに、コレクションの全要素が結果に入らないことがある。これはJoinメソッドがいわゆる「inner join」を実装してるから。つまり、キーが一致する要素だけを結合する。注文がない顧客は結果に出てこないし、顧客リストにいないCustomerIdの注文も結果に入らない。
でも、「注文がなくても全顧客を取得したい」場合はどうする?そういう時は「左側join」が必要で、LINQではGroupJoinとSelectManyの組み合わせで実現できる(これは次のレクチャーでやるよ)。クラシックなJoinだと、両方に要素がないと一致しないから、結果から落ちちゃうんだ。
7. 複数キー(composite key)での結合
たまに、1つじゃなくて複数のフィールドでコレクションを結合したいこともある。例えば「商品コード+販売年」で商品と売上を結合するみたいな。
LINQでは、匿名オブジェクトをキーとして作ることで解決できる:
var sales = ...; // 売上
var products = ...; // 商品
var query = products.Join(
sales,
prod => new { prod.Code, prod.Year },
sale => new { sale.ProductCode, sale.Year },
(prod, sale) => new { prod.Name, sale.Amount }
);
この匿名オブジェクトのプロパティ名と型が一致してることが大事。違うと、意味が同じでも一致しないから注意!
8. 結合後のコレクションのイメージ図
joinの動きを図でざっくり説明するとこんな感じ:
flowchart LR
subgraph Customers
A1["バシャ (Id=1)"]
A2["ペチャ (Id=2)"]
A3["マーシャ (Id=3)"]
end
subgraph Orders
B1["ペン (CustomerId=1)"]
B2["本 (CustomerId=2)"]
B3["ノート (CustomerId=2)"]
B4["消しゴム (CustomerId=3)"]
end
A1-->|Id=1|B1
A2-->|Id=2|B2
A2-->|Id=2|B3
A3-->|Id=3|B4
この図から分かるように、各顧客はキーが一致する全ての注文と結合される。
9. ミスや特徴をざっくりまとめ
よくあるミスの1つは、パラメータの順番を間違えること。特に両方のコレクションでキーの型が同じ時は注意。コンパイラやLINQはエラーを出さないけど、結果が空になったり、予想外のものになったりする。
もう1つのよくある落とし穴は、Joinで「左側結合」をやろうとすること。全顧客(注文がなくても)を結果に残したい場合、クラシックなJoinは一致するペアしか返さないから、対応する要素がないと結果に出てこないよ。
GO TO FULL VERSION