0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EF Core 10 の LeftJoin / RightJoin を理解する 〜Outer Join(外部結合)がシンプルに〜

Posted at

はじめに

業務で 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
    };

intoDefaultIfEmpty() の組み合わせが必要で、直感的とは言えません。

📌 開発者の声: 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 JOIN SQL に変換されます。


制限事項と注意点

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 は従来と同じなので、パフォーマンスを犠牲にせずコードをシンプルにできます。

実務での判断ポイント:

  1. .NET 10 を使える → 新規コードでは LeftJoin を積極採用
  2. クエリ構文が必要 → 従来パターンを継続
  3. 既存コードの移行 → 動作確認しながら段階的に
  4. 迷ったら → 生成 SQL は同じなので、可読性で選ぶ

参考文献

本記事は以下の情報源を参考に、筆者の実務経験を加えて執筆しました。

公式ドキュメント

GitHub Issues

GitHub Discussions

技術ブログ


最後まで読んでいただきありがとうございました!
質問やフィードバックがあれば、コメントでお知らせください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?