LINQ のJoin 操作

2 つのデータ ソースの "結合" とは、あるデータ ソースのオブジェクトを、共通の属性を共有する別のデータ ソースのオブジェクトと関連付けることです。

相互に直接つなげることができない関係を持つデータ ソースを対象とするクエリにおいて、Join は重要な操作になります。 オブジェクト指向プログラミングでは、結合は一方向の関係における逆方向など、モデル化されていないオブジェクト間の相関関係を意味する場合があります。 一方向の関係の例として、専攻を表す Department 型のプロパティを持つ Student クラスがあります。ただし、Department クラスには、Student オブジェクトのコレクションを表すプロパティはありません。 Department オブジェクトのリストから各学科のすべての学生を取得する場合は、結合操作を使用して学生を検索できます。

LINQ framework で用意された結合メソッドは JoinGroupJoin です。 この 2 つのメソッドは、等結合 (キーが等しいかどうかに基づいて 2 つのデータ ソースを対応させる結合) を実行します。 (比較に関して、Transact-SQL では、less than 演算子などの equals 以外の結合演算子がサポートされます)。リレーショナル データベース用語で説明すると、Join は内部結合 (両方のデータ セットで一致するオブジェクトだけが返される結合) を実装します。 リレーショナル データベース用語で GroupJoin メソッドに直接相当するものはありませんが、このメソッドは内部結合と左外部結合のスーパーセットを実装します。 左外部結合とは、最初 (左側) のデータ ソースの各要素を返す結合です。これらの要素は、もう一方のデータ ソースの要素と相関関係がなくても返されます。

次の図は、2 つのセットと、内部結合または左外部結合としてこれらのセットに含まれている要素の概念図を示しています。

Two overlapping circles showing inner/outer.

メソッド

メソッド名 説明 C# のクエリ式の構文 説明
Join キー セレクター関数に基づいて 2 つのシーケンスの Join を行い、値のペアを抽出します。 join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin キー セレクター関数に基づいて 2 つのシーケンスの Join を行い、各要素について結果として得られる一致をグループ化します。 join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

この記事の次の例では、この分野の一般的なデータ ソースが使用されています。

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}
public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

Student は、学年、主要学科、一連のスコアを持っています。 Teacher は、その教師が授業を受け持つキャンパスを示す City プロパティも持っています。 Department には名称と、学科長を務める Teacher への参照があります。

次の例では、join … in … on … equals … 句を使用し、特定の値に基づいて 2 つのシーケンスを結合します。

var query = from student in students
            join department in departments on student.DepartmentID equals department.ID
            select new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name };

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

上記のクエリは、次のコードのように、メソッド構文を使用して表すことができます。

var query = students.Join(departments,
    student => student.DepartmentID, department => department.ID,
    (student, department) => new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name });

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

次の例では、join … in … on … equals … into … 句を使用し、特定の値に基づいて 2 つのシーケンスを結合し、要素ごとに結果として得られる一致をグループ化します。

IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
                    join student in students on department.ID equals student.DepartmentID into studentGroup
                    select studentGroup;

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

上記のクエリは、次の例のように、メソッド構文を使用して表すことができます。

// Join department and student based on DepartmentId and grouping result
IEnumerable<IEnumerable<Student>> studentGroups = departments.GroupJoin(students,
    department => department.ID, student => student.DepartmentID,
    (department, studentGroup) => studentGroup);

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

内部結合の実行

リレーショナル データベースでは、"内部結合" により、2 番目のコレクション内の一致するすべての要素に対して、最初のコレクションの各要素が一度表示される結果セットが生成されます。 最初のコレクション内の要素に一致する要素が存在しない場合、その要素は結果セットには表示されません。 Join メソッドは、C# の join 句によって呼び出され、内部結合を実装します。 次の例は、4 種類の内部結合の実行方法を示しています。

  • 簡単なキーに基づいて、2 つのデータ ソースの要素を関連付ける単純な内部結合。
  • "複合" キーに基づいて、2 つのデータ ソースの要素を関連付ける内部結合。 複合キーは複数の値で構成され、複数のプロパティに基づいて要素を関連付けることができます。
  • 一連の結合操作が相互に追加された "複数の結合"。
  • グループ結合を使用して実装された内部結合。

単一キー結合

次の例では、Teacher オブジェクトを、その Teacherと一致する TeacherId を持つ Deparment オブジェクトと照合します。 C# の select 句では、オブジェクトの出力形式を定義します。 次の例では、結果として得られるオブジェクトは、学科名とその学科を率いる教師の名前で構成される匿名型です。

var query = from department in departments
            join teacher in teachers on department.TeacherID equals teacher.ID
            select new
            {
                DepartmentName = department.Name,
                TeacherName = $"{teacher.First} {teacher.Last}"
            };

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

以下の Join メソッド構文を使用すると、同じ結果が得られます。

var query = teachers
    .Join(departments, teacher => teacher.ID, department => department.TeacherID,
        (teacher, department) =>
        new { DepartmentName = department.Name, TeacherName = $"{teacher.First} {teacher.Last}" });

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

学科長ではない教師は、最終結果には表示されません。

複合キー結合

1 つのプロパティだけに基づいて要素を関連付ける代わりに、複合キーを使用して、複数のプロパティに基づいて要素を比較できます。 各コレクションに対してキー セレクター関数を指定し、比較するプロパティで構成された匿名型を返します。 プロパティにラベルを付ける場合は、各キーの匿名型に同じラベルを付ける必要があります。 また、プロパティは、同じ順序で表示する必要があります。

次の例では、Teacher オブジェクトのリストと Student オブジェクトのリストを使用して、学生でもある教師を調べます。 どちらの型にも、各人の姓と名を表すプロパティがあります。 それぞれのリストの要素から結合キーを作成する関数が、そのプロパティで構成された匿名型を返します。 結合操作により、これらの複合キーが等しいかどうかが比較され、それぞれのリストの姓名が一致するオブジェクトのペアが返されます。

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query =
    from teacher in teachers
    join student in students on new
    {
        FirstName = teacher.First,
        LastName = teacher.Last
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select teacher.First + " " + teacher.Last;

string result = "The following people are both teachers and students:\r\n";
foreach (string name in query)
{
    result += $"{name}\r\n";
}
Console.Write(result);

次の例に示すように、Join メソッドを使用できます。

IEnumerable<string> query = teachers
    .Join(students,
        teacher => new { FirstName = teacher.First, LastName = teacher.Last },
        student => new { student.FirstName, student.LastName },
        (teacher, student) => $"{teacher.First} {teacher.Last}"
 );

Console.WriteLine("The following people are both teachers and students:");
foreach (string name in query)
{
    Console.WriteLine(name);
}

複数の結合

任意の数の結合操作を相互に追加して、複数の結合を実行できます。 C# の各 join 句は、指定されたデータ ソースを前の結合の結果に関連付けます。

最初の join 句は、Department オブジェクトの ID と一致する Student オブジェクトの DepartmentID に基づいて学生と学科を照合します。 この操作で、Student オブジェクトと Department オブジェクトが含まれた匿名型のシーケンスが返されます。

2 番目の join 句は、最初の結合によって返された匿名型を、学科長 ID と一致する教師の ID に基づく Teacher オブジェクトと関連付けます。 この操作で、学生名、学科名、学科長名を含む匿名型のシーケンスが返されます。 この操作は内部結合であるため、2 番目のデータ ソースに一致するものが存在する、最初のデータ ソースのオブジェクトのみが返されます。

// The first join matches Department.ID and Student.DepartmentID from the list of students and
// departments, based on a common ID. The second join matches teachers who lead departments
// with the students studying in that department.
var query = from student in students
    join department in departments on student.DepartmentID equals department.ID
    join teacher in teachers on department.TeacherID equals teacher.ID
    select new {
        StudentName = $"{student.FirstName} {student.LastName}",
        DepartmentName = department.Name,
        TeacherName = $"{teacher.First} {teacher.Last}"
    };

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

複数の Join メソッドを使用する次の同等の例では、匿名型で同じアプローチが使用されます。

var query = students
    .Join(departments, student => student.DepartmentID, department => department.ID,
        (student, department) => new { student, department })
    .Join(teachers, commonDepartment => commonDepartment.department.TeacherID, teacher => teacher.ID,
        (commonDepartment, teacher) => new
        {
            StudentName = $"{commonDepartment.student.FirstName} {commonDepartment.student.LastName}",
            DepartmentName = commonDepartment.department.Name,
            TeacherName = $"{teacher.First} {teacher.Last}"
        });

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

グループ化結合を使用した内部結合

グループ結合を使用して内部結合を実装する方法を次の例に示します。 Department オブジェクトのリストは、Student.DepartmentID プロパティと一致する Department.ID に基づいて、Student オブジェクトのリストにグループ結合されます。 グループ結合によって、それぞれのグループが Department オブジェクトおよび一致する Student オブジェクトのシーケンスで構成された、中間グループのコレクションが作成されます。 2 番目の from 句は、このシーケンスのシーケンスを 1 つの長いシーケンスに結合 (またはフラット化) します。 select 句は、最終シーケンス内の要素の型を指定します。 この型は学生名と、一致する学科名で構成される匿名型です。

var query1 =
    from department in departments
    join student in students on department.ID equals student.DepartmentID into gj
    from subStudent in gj
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
    };
Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in query1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

次に示すように GroupJoin メソッドを使用すると、同じ結果を得ることができます。

var queryMethod1 = departments
    .GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, gj) => new { department, gj })
    .SelectMany(departmentAndStudent => departmentAndStudent.gj,
        (departmentAndStudent, subStudent) => new
        {
            DepartmentName = departmentAndStudent.department.Name,
            StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
        });

Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in queryMethod1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

結果は、into 句のない join 句を使用して内部結合を実行することで得られる結果セットと同じです。 次のコードは、この同等のクエリを示しています。

var query2 = from department in departments
    join student in students on department.ID equals student.DepartmentID
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    };

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in query2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

チェーンを回避するために、次に示すような 1 つの Join メソッドを使用できます。

var queryMethod2 = departments.Join(students, departments => departments.ID, student => student.DepartmentID,
    (department, student) => new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    });

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in queryMethod2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

グループ化結合の実行

グループ結合は、階層データ構造を作成する場合に便利です。 これは、最初のコレクションの各要素と、2 番目のコレクションの相関関係を持つ要素のセットを組み合わせたものです。

Note

最初のコレクションの各要素は、2 番目のコレクションに相関関係を持つ要素があるかどうかにかかわらず、グループ結合の結果セットに表示されます。 相関関係を持つ要素が見つからない場合、その要素の相関関係を持つ要素のシーケンスは空です。 そのため、結果セレクターは最初のコレクションのすべての要素にアクセスできます。 これは、非グループ結合の結果セレクターとは異なります。非グループ結合の結果セレクターは、2 番目のコレクションに一致するものがない最初のコレクションの要素にアクセスすることはできません。

警告

Enumerable.GroupJoin には、従来のリレーショナル データベースの用語に直接相当するものはありません。 ただし、このメソッドでは内部結合と左外部結合のスーパーセットが実装されます。 これらの操作はどちらも、グループ化結合の観点から記述できます。 詳しくは、「Entity Framework Core、GroupJoin」をご覧ください。

この記事の最初の例では、グループ結合を実行する方法を示します。 2 つ目の例では、グループ結合を使用して XML 要素を作成する方法を示します。

グループ結合

次の例では、Student.DepartmentID プロパティと一致する Deoartment.ID に基づいて、Department 型と Student 型のオブジェクトのグループ結合を実行します。 一致ごとに要素のペアを生成する非グループ結合と異なり、グループ結合では最初のコレクションの要素ごとに 1 つのオブジェクト (この例では Department オブジェクト) のみを生成します。 2 番目のコレクションの対応する要素 (この例では Student オブジェクト) が 1 つのコレクションにグループ化されます。 最後に、結果セレクター機能により、Department.Name と、Student オブジェクトのコレクションで構成される一致ごとに匿名型が作成されます。

var query = from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new
    {
        DepartmentName = department.Name,
        Students = studentGroup
    };

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

上記の例では、query 変数には、リストを作成するクエリが含まれます。リストの各要素は、学科名とその学科で学ぶ学生のコレクションが含まれる匿名型です。

次のコードでは、メソッド構文を使用した同等のクエリを示しています。

var query = departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
    (department, Students) => new { DepartmentName = department.Name, Students });

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

XML を作成するグループ結合

グループ結合は、LINQ to XML を使用した XML の作成に適しています。 次の例は前の例に似ていますが、匿名型を作成するのではなく、結果セレクター機能により、結合されたオブジェクトを表す XML 要素を作成する点が異なります。

XElement departmentsAndStudents = new("DepartmentEnrollment",
    from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new XElement("Department",
        new XAttribute("Name", department.Name),
        from student in studentGroup
        select new XElement("Student",
            new XAttribute("FirstName", student.FirstName),
            new XAttribute("LastName", student.LastName)
        )
    )
);

Console.WriteLine(departmentsAndStudents);

次のコードでは、メソッド構文を使用した同等のクエリを示しています。

XElement departmentsAndStudents = new("DepartmentEnrollment",
    departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, Students) => new XElement("Department",
            new XAttribute("Name", department.Name),
            from student in Students
            select new XElement("Student",
                new XAttribute("FirstName", student.FirstName),
                new XAttribute("LastName", student.LastName)
            )
        )
    )
);

Console.WriteLine(departmentsAndStudents);

左外部結合の実行

左外部結合は、最初のコレクションの各要素を、2 つ目のコレクション内にある要素との相関関係の有無にかかわらず返す結合です。 LINQ を使用すると、グループ結合の結果に対して DefaultIfEmpty メソッドを呼び出すことで、左外部結合を実行できます。

次の例は、グループ結合の結果に対して DefaultIfEmpty メソッドを使用し、左外部結合を実行する方法を示しています。

2 つのコレクションの左外部結合を作成するための最初のステップは、グループ結合を使用して内部結合を実行することです。 (このプロセスの詳細については、「内部結合の実行」参照してください。)この例ではDepartment オブジェクトのリストが、学生のDepartmentID に一致する Department オブジェクトの ID に基づいて、Student オブジェクトのリストに内部結合されています。

2 つ目のステップは、最初 (左側) のコレクションの各要素を結果セットに含めることです。このとき、その要素と一致するものが右のコレクションにあるかどうかは考慮しません。 これを行うには、グループ結合内の一致する要素の各シーケンスに対して、DefaultIfEmpty を呼び出します。 この例では、Student オブジェクトに一致する各シーケンスに対して、DefaultIfEmpty が呼び出されています。 このメソッドは、任意の Department オブジェクトに対して一致する Student オブジェクトのシーケンスが空である場合に、単一の既定値を含むコレクションを返します。これにより、結果コレクション内に各 Department オブジェクトが表されることが保証されます。

Note

参照型の既定値は null です。そのためこのコード例では、各 Student コレクションの各要素にアクセスする前に Null 参照がチェックされます。

var query =
    from student in students
    join department in departments on student.DepartmentID equals department.ID into gj
    from subgroup in gj.DefaultIfEmpty()
    select new
    {
        student.FirstName,
        student.LastName,
        Department = subgroup?.Name ?? string.Empty
    };

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

次のコードでは、メソッド構文を使用した同等のクエリを示しています。

var query = students.GroupJoin(departments, student => student.DepartmentID, department => department.ID,
    (student, department) => new { student, subgroup = department.DefaultIfEmpty() })
    .Select(gj => new
    {
        gj.student.FirstName,
        gj.student.LastName,
        Department = gj.subgroup?.FirstOrDefault()?.Name ?? string.Empty
    });

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

関連項目