次の方法で共有


LINQ のJoin 操作

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

重要

これらのサンプルでは、System.Collections.Generic.IEnumerable<T> データ ソースを使用します。 System.Linq.IQueryProvider に基づくデータ ソースでは、System.Linq.IQueryable<T> データ ソースと式ツリーが使用されます。 式ツリーには、許可される C# 構文に制限があります。 さらに、IQueryProvider などの各 データ ソースでは、より多くの制限が課される場合があります。 ご利用のデータ ソースのドキュメントをご覧ください。

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

LINQ フレームワークで提供される結合メソッドは、JoinGroupJoinです。 この 2 つのメソッドは、等結合 (キーが等しいかどうかに基づいて 2 つのデータ ソースを対応させる結合) を実行します。 (比較のために、Transact-SQL では equals以外の結合演算子 (たとえば、less than 演算子) がサポートされます)。リレーショナル データベース用語では、Join は内部結合を実装します。これは、他のデータ セットに一致するオブジェクトのみが返される結合の種類です。 リレーショナル データベース用語で GroupJoin メソッドに直接相当するものはありませんが、このメソッドは内部結合と左外部結合のスーパーセットを実装します。 左外部結合は、最初の (左) データ ソースの各要素を返す結合です。他のデータ ソースに相関する要素がない場合でも、その要素が返されます。

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

内側/外側を示す 2 つの重なり合う円。

メソッド

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

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

Queryable.GroupJoin

Note

この記事の次の例では、この領域の共通データ ソースを使用します。
Student は、学年、主要学科、一連のスコアを持っています。 Teacher は、その教師が授業を受け持つキャンパスを示す City プロパティも持っています。 Department は名称と、学科長を務める Teacher への参照を持っています。
サンプル データ セットは、 source リポジトリにあります。

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; }
}

次の例では、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 番目のコレクション内の一致する要素ごとに 1 回出現する結果セットを生成します。 最初のコレクション内の要素に一致する要素が存在しない場合、その要素は結果セットには表示されません。 Join メソッドは、C# の join 句によって呼び出され、内部結合を実装します。 次の例は、内部結合の 4 つのバリエーションを実行する方法を示しています。

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

単一キー結合

次の例では、Teacher オブジェクトを、その Departmentと一致する TeacherId を持つ Teacher オブジェクトと照合します。 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 句は、Student オブジェクトの DepartmentID と一致する Department オブジェクトの ID に基づいて学生と学科を照合します。 この操作で、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 プロパティと一致する Department.ID に基づいて、Student.DepartmentID オブジェクトのリストにグループ結合されます。 グループ結合によって中間グループのコレクションが作成されます。各グループは、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 プロパティに一致する Department.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 オブジェクトのリストが、学生のStudent に一致する Department オブジェクトの ID に基づいて、DepartmentID オブジェクトのリストに内部結合されています。

2 つ目のステップは、最初 (左側) のコレクションの各要素を結果セットに含めることです。このとき、その要素と一致するものが右のコレクションにあるかどうかは考慮しません。 これは、グループ結合から一致する要素の各シーケンスで DefaultIfEmpty を呼び出すことによって実現されます。 この例では、DefaultIfEmpty オブジェクトに一致する各シーケンスに対して、Student が呼び出されています。 このメソッドは、任意の Student オブジェクトに対して一致する Department オブジェクトのシーケンスが空である場合に、単一の既定値を含むコレクションを返します。これにより、結果コレクション内に各 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, departmentList) => new { student, subgroup = departmentList })
    .SelectMany(
        joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
        (student, department) => new
        {
            student.student.FirstName,
            student.student.LastName,
            Department = department.Name
        });

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

関連項目