はじめに
業務で Entity Framework Core を使っていて、LEFT JOIN を書くたびに「なぜこんなに複雑なのか」と思っていました。調べてみると、LINQ には長らく LeftJoin 演算子がなく、GroupJoin + SelectMany + DefaultIfEmpty という3つの演算子を組み合わせる必要があることがわかりました。
.NET 10 / EF Core 10 で、ついに LeftJoin() / RightJoin() が追加され、この問題が解決されました。従来パターンとの比較から実務での使い方までまとめます。
この記事で学べること
- 従来の外部結合パターンが複雑だった理由
-
LeftJoin()/RightJoin()の使い方と生成SQL - 現時点での制限事項(クエリ構文未対応など)
- 実務での移行判断ガイド
対象読者
- EF Core で外部結合を書いたことがある方
- .NET 10 へのアップグレードを検討している方
⚠️ 本記事は EF Core 10.0 / .NET 10 を対象としています(2025年12月時点)。
LeftJoin()/RightJoin()は .NET 10 で導入された機能です。
サンプルのデータモデル
本記事では、ECサイトの商品管理を例に説明します。
Product(商品)
└── Reviews(レビュー) 1対多(レビューがない商品もある)
エンティティ定義
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Review
{
public int Id { get; set; }
public int ProductId { get; set; }
public int Rating { get; set; }
public string Comment { get; set; }
public Product Product { get; set; }
}
従来の外部結合パターン
3つの演算子を組み合わせる複雑さ
商品とレビューを LEFT JOIN で取得したい場合、従来はこう書く必要がありました。
// 従来パターン(GroupJoin + SelectMany + DefaultIfEmpty)
var query = dbContext.Products
.GroupJoin(
dbContext.Reviews,
product => product.Id,
review => review.ProductId,
(product, reviews) => new { product, reviews })
.SelectMany(
x => x.reviews.DefaultIfEmpty(),
(x, review) => new
{
ProductId = x.product.Id,
ProductName = x.product.Name,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "レビューなし"
});
一見問題なさそうですが、このパターンには問題があります。
| 問題 | 説明 |
|---|---|
| 可読性が低い | 3つの演算子の組み合わせで意図が伝わりにくい |
| 書き方を覚えにくい | 毎回検索が必要になる |
| 中間オブジェクトが必要 |
new { product, reviews } のような匿名型を経由 |
クエリ構文でも複雑
// クエリ構文(従来)
var query =
from product in dbContext.Products
join review in dbContext.Reviews
on product.Id equals review.ProductId into reviewGroup
from review in reviewGroup.DefaultIfEmpty()
select new
{
ProductId = product.Id,
ProductName = product.Name,
Rating = (int?)review.Rating ?? 0
};
into と DefaultIfEmpty() の組み合わせが必要で、直感的とは言えません。
📌 開発者の声: GitHub Issue efcore#12793 では、LINQでの外部結合が複雑で保守が困難であると報告されていました。
LeftJoin / RightJoin でシンプルに
使い方
.NET 10 / EF Core 10 では、LeftJoin() を使うだけで外部結合が書けます。
// ✅ 新パターン(EF Core 10)
var query = dbContext.Products
.LeftJoin(
dbContext.Reviews,
product => product.Id,
review => review.ProductId,
(product, review) => new
{
ProductId = product.Id,
ProductName = product.Name,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "レビューなし"
});
従来パターンとの比較
| 観点 | 従来パターン | LeftJoin |
|---|---|---|
| 使用演算子 | 3つ(GroupJoin, SelectMany, DefaultIfEmpty) | 1つ |
| 中間オブジェクト | 必要 | 不要 |
| 意図の明確さ | △ パターンを知らないと理解困難 | ◎ メソッド名で明確 |
RightJoin の使い方
// RightJoin: 第2引数(Products)をすべて保持して結合
var query = dbContext.Reviews
.RightJoin(
dbContext.Products,
review => review.ProductId,
product => product.Id,
(review, product) => new
{
ProductId = product.Id,
ProductName = product.Name,
Rating = (int?)review.Rating ?? 0
});
生成される SQL
LeftJoin の生成 SQL
SELECT
p."Id" AS "ProductId",
p."Name" AS "ProductName",
COALESCE(r."Id", 0) AS "ReviewId",
COALESCE(r."Rating", 0) AS "Rating",
COALESCE(r."Comment", 'レビューなし') AS "Comment"
FROM "Products" AS p
LEFT JOIN "Reviews" AS r ON p."Id" = r."ProductId"
従来パターンとの比較:生成 SQL は同一
従来の GroupJoin + DefaultIfEmpty パターンと新しい LeftJoin は、同一の SQL を生成します。
| 方式 | 生成 SQL | パフォーマンス |
|---|---|---|
| 従来パターン | LEFT JOIN | 同一 |
| 新 LeftJoin | LEFT JOIN | 同一 |
📌 パフォーマンスについて: データベースレベルでの性能差はありません。利点は主にコードの可読性と保守性です。RightJoin も同様に、
RIGHT JOINSQL に変換されます。
制限事項と注意点
1. クエリ構文(from ... select)は未対応
// ❌ これは書けない(2025年12月時点)
var query =
from product in dbContext.Products
left join review in dbContext.Reviews // ← コンパイルエラー
on product.Id equals review.ProductId
select new { ... };
メソッド構文(.LeftJoin())のみ使用可能です。
📌 今後の展望: クエリ構文への
left join/right joinキーワード追加は GitHub Discussion csharplang#8892 で提案されていますが、C# 14 には含まれていません。
2. .NET 10 が必須
| 環境 | 対応状況 |
|---|---|
| .NET 10 | ✅ 対応 |
| .NET 9 以前 | ❌ 非対応 |
| .NET Framework | ❌ 非対応 |
3. null 処理は明示的に
LeftJoin の結果セレクタでは、右側(TInner)が null になる可能性があります。
// ✅ 良い例:null 処理を明示
(product, review) => new
{
ProductName = product.Name,
Rating = (int?)review.Rating ?? 0, // ← null 合体演算子
Comment = review.Comment ?? "レビューなし"
}
// ❌ 悪い例:null 処理なし
(product, review) => new
{
ProductName = product.Name,
Rating = review.Rating, // ← NullReferenceException の可能性
Comment = review.Comment
}
実務での移行判断
移行すべきケース
| 状況 | 理由 |
|---|---|
| 新規プロジェクト(.NET 10) | 可読性向上、標準パターンとして採用 |
| 外部結合が多いコードベース | 保守性が大幅に向上 |
| チームに LINQ 初心者がいる | 学習コスト削減 |
移行を待つべきケース
| 状況 | 理由 |
|---|---|
| .NET 10 へのアップグレードが困難 | ランタイム依存 |
| クエリ構文を多用している | メソッド構文への書き換えが必要 |
| 既存コードが安定稼働中 | 動作するコードを変える必要はない |
移行パターン
// ✅ Before(従来)
.GroupJoin(inner, o => o.Key, i => i.Key, (o, g) => new { o, g })
.SelectMany(x => x.g.DefaultIfEmpty(), (x, i) => new { x.o, i })
// ✅ After(.NET 10)
.LeftJoin(inner, o => o.Key, i => i.Key, (o, i) => new { o, i })
📌 段階的な移行: 生成SQLは同一なので、一度に全て書き換える必要はありません。新規コードから順次採用することをおすすめします。
まとめ
| 観点 | 従来パターン | LeftJoin / RightJoin |
|---|---|---|
| 導入バージョン | — | .NET 10 / EF Core 10 |
| コードの可読性 | △ 複雑 | ◎ シンプル |
| 生成 SQL | LEFT JOIN | LEFT JOIN / RIGHT JOIN |
| クエリ構文 | ✅ 対応(複雑) | ❌ 未対応 |
LeftJoin / RightJoin は、長らく開発者が待ち望んでいた機能です。生成される SQL は従来と同じなので、パフォーマンスを犠牲にせずコードをシンプルにできます。
実務での判断ポイント:
- .NET 10 を使える → 新規コードでは LeftJoin を積極採用
- クエリ構文が必要 → 従来パターンを継続
- 既存コードの移行 → 動作確認しながら段階的に
- 迷ったら → 生成 SQL は同じなので、可読性で選ぶ
参考文献
本記事は以下の情報源を参考に、筆者の実務経験を加えて執筆しました。
公式ドキュメント
GitHub Issues
- API Proposal: Introduce LeftJoin LINQ operator - dotnet/runtime#110292
- Support the new .NET 10 LeftJoin operators - dotnet/efcore#12793
GitHub Discussions
技術ブログ
最後まで読んでいただきありがとうございました!
質問やフィードバックがあれば、コメントでお知らせください。