【C#】LINQで外部結合(Left Outer Join)を実現する方法

データベース操作における「左外部結合(LEFT OUTER JOIN)」は、左側のテーブルの全レコードを保持しつつ、右側のテーブルにマッチするレコードがあれば結合し、なければNULLとして扱う操作です。

C#のLINQにおいてこの操作をメソッド構文で実現するには、GroupJoinメソッドとSelectManyメソッド、そしてDefaultIfEmptyメソッドを組み合わせるテクニックが必要です。

今回は、タスク管理システムにおける「タスク」と「カテゴリ」の関係を題材に、カテゴリが削除されていたり未設定であったりする場合でも、タスク一覧を欠損させずに取得する実装方法を解説します。


目次

外部結合(Left Outer Join)の仕組み

LINQのメソッド構文で外部結合を行う手順は以下の通りです。

  1. GroupJoin: 親(左側)と子(右側)をキーで紐付け、階層構造を作ります。
  2. SelectMany: 階層化された子要素をフラットに展開します。
  3. DefaultIfEmpty: 子要素が存在しない(紐付くデータがない)場合に、その部分をデフォルト値(null)として扱います。

実践的なコード例:タスクとカテゴリの結合

以下のコードは、タスクリスト(左側)に対し、カテゴリリスト(右側)を外部結合する例です。カテゴリIDが一致しないタスクも、一覧から消えることなく表示されます。

using System;
using System.Collections.Generic;
using System.Linq;

namespace TaskManager
{
    // タスク情報(左側のデータソース)
    public class TaskItem
    {
        public string Title { get; set; }
        public int CategoryId { get; set; }
    }

    // カテゴリ情報(右側のデータソース)
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    class Program
    {
        static void Main()
        {
            // シナリオ:
            // タスク一覧を表示したいが、中には「存在しないカテゴリID」が紐付いているタスクがある。
            // 内部結合(Join)を使うと、そのようなタスクは結果から消えてしまう。
            // 外部結合(Left Join)を使い、カテゴリ不明のタスクも表示させる。

            var tasks = new List<TaskItem>
            {
                new TaskItem { Title = "牛乳を買う", CategoryId = 1 },     // Category: 生活
                new TaskItem { Title = "C#の学習", CategoryId = 2 },       // Category: 勉強
                new TaskItem { Title = "部屋の掃除", CategoryId = 1 },     // Category: 生活
                new TaskItem { Title = "謎の用事", CategoryId = 99 },      // Category: 存在しないID
            };

            var categories = new List<Category>
            {
                new Category { Id = 1, Name = "生活" },
                new Category { Id = 2, Name = "勉強" },
                new Category { Id = 3, Name = "仕事" },
            };

            // LINQによる外部結合の実装
            var query = tasks
                // 1. GroupJoinでタスクに該当するカテゴリのグループを作成
                .GroupJoin(
                    categories,
                    task => task.CategoryId, // 左側のキー
                    category => category.Id, // 右側のキー
                    (task, categoryCollection) => new { Task = task, Categories = categoryCollection }
                )
                // 2. SelectManyでフラット化しつつ、DefaultIfEmptyで空対策を行う
                .SelectMany(
                    x => x.Categories.DefaultIfEmpty(), // マッチするカテゴリがない場合はnullを含む空集合にする
                    (x, category) => new
                    {
                        TaskTitle = x.Task.Title,
                        // categoryがnullの場合は未設定扱いとする
                        CategoryName = category == null ? "(カテゴリ未設定)" : category.Name
                    }
                );

            // 結果の出力
            Console.WriteLine("--- タスク一覧 ---");
            foreach (var item in query)
            {
                Console.WriteLine($"タスク: {item.TaskTitle,-10} | カテゴリ: {item.CategoryName}");
            }
        }
    }
}

実行結果

--- タスク一覧 ---
タスク: 牛乳を買う      | カテゴリ: 生活
タスク: C#の学習       | カテゴリ: 勉強
タスク: 部屋の掃除      | カテゴリ: 生活
タスク: 謎の用事       | カテゴリ: (カテゴリ未設定)

「謎の用事」はカテゴリIDが99であり、categoriesリストには存在しませんが、結果から除外されずに「(カテゴリ未設定)」として出力されています。これが外部結合の効果です。


技術的なポイント

1. DefaultIfEmptyの役割

このメソッドが外部結合の核となります。GroupJoinの結果、マッチする相手がいない場合、空のシーケンスが生成されます。DefaultIfEmpty()を呼び出すことで、「空のシーケンス」を「null要素を1つ含むシーケンス」に変換します。これにより、後続のSelectManyがループを回せるようになり、結果として左側のデータが残ります。

2. クエリ構文(Query Syntax)の場合

メソッド構文では記述がやや冗長になりますが、クエリ構文を使用するとSQLに近い形で直感的に記述可能です。

// クエリ構文での記述例
var querySyntax = from t in tasks
                  join c in categories on t.CategoryId equals c.Id into groupJoin
                  from subC in groupJoin.DefaultIfEmpty() // ここで外部結合
                  select new
                  {
                      TaskTitle = t.Title,
                      CategoryName = subC?.Name ?? "(カテゴリ未設定)"
                  };

チームのコーディング規約や可読性の好みに応じて使い分けてください。

まとめ

GroupJoinSelectManyDefaultIfEmptyを組み合わせることで、LINQでもSQLのLeft Outer Joinと同様の処理が可能です。マスタデータが存在しないトランザクションデータを扱う際や、オプション項目を結合する際など、データの欠損を防ぎたい場面で必須となるテクニックです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次