【LINQ】GroupJoinを理解する
概要
LINQのGroupJoin
がどんな挙動をするのか、たまに忘れます。
そろそろ正確に理解したいなと思い、この記事を書きました。
GroupJoinの使い方
GroupJoinは、1対多の関係にある2つのコレクションを、キーで関連付けして、親とその親に関連する子のグループを作るメソッドです。親のデータごとに、対応する子のコレクションを取得したい時に便利です。
まず、各引数の役割について整理します。
var test = parents.GroupJoin(
children, // 子のリスト
parent => parent.Id, // 親のキー
child => child.ParentId, // 子のキー
// ↓ parentは親の要素 / childrenは上で指定したキーで関連付けされたchildをグループ化したもの(IEnumerable<Child>)
(parent, children) => new // 結合結果の調整
{
ParentName = parent.Name,
Children = children.Select(c => c.Name),
}
);
続いて、実例を元に動きを確認します。
以下のようなクラスとサンプルデータがあるとします。ここでは、親がTeam
(チーム)で、子がPlayer
(選手)です。Team:Playerが1:多の関係になります。
public class Team
{
public string Name { get; set; } = string.Empty;
public string Stadium { get; set; } = string.Empty;
}
public class Player
{
public string Name { get; set; } = string.Empty;
public string TeamName { get; set; } = string.Empty;
}
var teams = new List<Team>
{
new() { Name = "横浜DeNAベイスターズ", Stadium = "横浜スタジアム" },
new() { Name = "読売ジャイアンツ", Stadium = "東京ドーム" },
new() { Name = "東京ヤクルトスワローズ", Stadium = "神宮球場" },
new() { Name = "阪神タイガース", Stadium = "甲子園球場" },
new() { Name = "広島東洋カープ", Stadium = "マツダ スタジアム" },
new() { Name = "中日ドラゴンズ", Stadium = "バンテリンドーム ナゴヤ" },
};
var players = new List<Player>
{
new() { Name = "牧秀悟", TeamName = "横浜DeNAベイスターズ" },
new() { Name = "宮崎敏郎", TeamName = "横浜DeNAベイスターズ" },
new() { Name = "筒香嘉智", TeamName = "横浜DeNAベイスターズ" },
new() { Name = "村上宗隆", TeamName = "東京ヤクルトスワローズ" },
new() { Name = "坂本勇人", TeamName = "読売ジャイアンツ" },
new() { Name = "山田哲人", TeamName = "東京ヤクルトスワローズ" },
};
では、GroupJoinを用いて、チームごとに選手をグループ化して、その結果を出力します。
var results = teams.GroupJoin(
players, // 結合対象(子側)
team => team.Name, // 親(チーム)のキー
player => player.TeamName, // 子(選手)のキー
(team, teamPlayers) => new
{
// チーム名、球場名、(チームに紐づく)選手名のコレクションを持つオブジェクトを作成
TeamName = team.Name,
Stadium = team.Stadium,
Players = teamPlayers.Select(p => p.Name),
}
);
foreach (var team in results)
{
Console.WriteLine($"{team.TeamName}({team.Stadium})の選手一覧:");
foreach (var player in team.Players)
{
Console.WriteLine($" - {player}");
}
}
結果は以下のようになります。
横浜DeNAベイスターズ(横浜スタジアム)の選手一覧:
- 牧秀悟
- 宮崎敏郎
- 筒香嘉智
読売ジャイアンツ(東京ドーム)の選手一覧:
- 坂本勇人
東京ヤクルトスワローズ(神宮球場)の選手一覧:
- 村上宗隆
- 山田哲人
阪神タイガース(甲子園球場)の選手一覧:
広島東洋カープ(マツダ スタジアム)の選手一覧:
中日ドラゴンズ(バンテリンドーム ナゴヤ)の選手一覧:
チームごとに選手をグループ化できていることが確認できます。また、グループ化する対象(ここでは選手)がいない場合も結果に含まれることが分かります。
SelectManyと一緒に使うと左外部結合になる
SelectManyとは
SelectMany
は、親データに紐づく子データのコレクションをフラット化し、各親と子の組み合わせを1つずつ展開するメソッドです。このとき、生成されるデータの数は、各親に対してフラット化される子要素の数と同じになります。
GroupJoinとSelectManyをセットで使う
SelectMany
は、GroupJoin
とセットで使うことで、左外部結合(LEFT JOIN
)に近い動作を実現できます。これはよく使われる手法なので、GroupJoin
単体の使い方と合わせて確認していきます。
では、先ほどと同じデータを使って、GroupJoin
の後にSelectMany
を追加して、選手単位でフラット化してみます。
var results = teams.GroupJoin(
players,
team => team.Name,
player => player.TeamName,
(team, teamPlayers) => new
{
TeamName = team.Name,
Stadium = team.Stadium,
Players = teamPlayers.DefaultIfEmpty(),
}
)
.SelectMany(
// 第一引数には、フラット化する対象のコレクションを指定
teamWithPlayers => teamWithPlayers.Players,
// 第二引数では、フラット化されたコレクションの各要素と親のデータを使って、新しいオブジェクトを作成する方法を指定
// teamWithPlayers: GroupJoinで作成した選手のコレクションを持つチームのデータ
// player: 第一引数で指定した選手のコレクションから1つずつ取り出した要素
(teamWithPlayers, player) => new
{
TeamName = teamWithPlayers.TeamName,
Stadium = teamWithPlayers.Stadium,
PlayerName = player?.Name ?? "なし"
}
);
出力結果は次のようになります。
横浜DeNAベイスターズ(横浜スタジアム)の選手: 牧秀悟
横浜DeNAベイスターズ(横浜スタジアム)の選手: 宮崎敏郎
横浜DeNAベイスターズ(横浜スタジアム)の選手: 筒香嘉智
読売ジャイアンツ(東京ドーム)の選手: 坂本勇人
東京ヤクルトスワローズ(神宮球場)の選手: 村上宗隆
東京ヤクルトスワローズ(神宮球場)の選手: 山田哲人
阪神タイガース(甲子園球場)の選手: なし
広島東洋カープ(マツダ スタジアム)の選手: なし
中日ドラゴンズ(バンテリンドーム ナゴヤ)の選手: なし
子要素(選手)ごとにデータが生成されていることが確認できます。
注意する点としては、GroupJoin
でDefaultIfEmpty()
を指定している点です。
(team, teamPlayers) => new
{
..略
Players = teamPlayers.DefaultIfEmpty(),
}
DefaultIfEmpty()
を指定しない場合、選手コレクションを持たないチームは表示されません。これは SelectMany
が子のコレクション(選手)をフラット化する際に、子の要素が空(存在しない)場合、その親のデータが除外されるためです。この動作は、SQLの内部結合(INNER JOIN
)に似た挙動です。
そのため、左外部結合(LEFT JOIN
)のように、選手を持たないチームも表示したい場合には、DefaultIfEmpty()
を使用する必要があります。DefaultIfEmpty()
を使うと、選手が存在しなくても、Playersがnull
として指定され、親のレコードが残るようになります。これにより、選手がいない場合でもチームは結果に残り、選手名が "なし"
として表示されます。
参考
最後に
以上です。
LINQのGroupJoin
やSelectMany
について整理しました。
未来の自分のために書いた備忘的な要素が強めですが、何かのお役に立てばなお嬉しいです。
Discussion