結合は、あるデータ ソース内のオブジェクトを、別のデータ ソースで共通の属性を共有するオブジェクトに関連付けます。
重要
これらのサンプルでは、System.Collections.Generic.IEnumerable<T> データ ソースを使用します。
System.Linq.IQueryProvider に基づくデータ ソースでは、System.Linq.IQueryable<T> データ ソースと式ツリーが使用されます。 式ツリーには、許可される C# 構文に制限があります。 さらに、IQueryProvider などの各 データ ソースでは、より多くの制限が課される場合があります。 ご利用のデータ ソースのドキュメントをご覧ください。
結合は、相互のリレーションシップを直接フォローできないデータ ソースを対象とするクエリの重要な操作です。 オブジェクト指向プログラミングでは、結合は一方向の関係における逆方向など、モデル化されていないオブジェクト間の相関関係を意味する場合があります。 一方向の関係の例として、専攻を表す Student 型のプロパティを持つ Department クラスがあります。ただし、Department クラスには、Student オブジェクトのコレクションを表すプロパティはありません。
Department オブジェクトの一覧があり、各部署のすべての学生を検索する場合は、結合操作を使用してそれらを検索できます。
LINQ フレームワークには、結合メソッド ( Join と GroupJoin) が用意されています。 この 2 つのメソッドは、等結合 (キーが等しいかどうかに基づいて 2 つのデータ ソースを対応させる結合) を実行します。 比較のために、Transact-SQL では、equals演算子など、less than以外の結合演算子がサポートされています。 リレーショナル データベースの用語では、 Join は内部結合を実装します。これは、他のデータ セットに一致するオブジェクトのみが返される結合の種類です。 リレーショナル データベース用語で GroupJoin メソッドに直接相当するものはありませんが、このメソッドは内部結合と左外部結合のスーパーセットを実装します。 左外部結合は、最初の (左) データ ソースの各要素を返す結合です。他のデータ ソースに相関する要素がない場合でも、その要素が返されます。
次の図は、2 つのセットと、内部結合または左外部結合に含まれるセット内の要素の概念図を示しています。
メソッド
| メソッド名 | 説明 | C# のクエリ式の構文 | その他の情報 |
|---|---|---|---|
| Join | キー セレクター関数に基づいて 2 つのシーケンスを結合し、値のペアを抽出します。 | join … in … on … equals … |
Enumerable.Join Queryable.Join |
| GroupJoin | キー セレクター関数に基づいて 2 つのシーケンスを結合し、各要素について結果として得られる一致をグループ化します。 | join … in … on … equals … into … |
Enumerable.GroupJoin Queryable.GroupJoin |
| LeftJoin | 一致するキーに基づいて、2 つのシーケンスの要素を関連付けます。 | N/A | Enumerable.LeftJoin Queryable.LeftJoin |
| RightJoin | 一致するキーに基づいて、2 つのシーケンスの要素を関連付けます。 | N/A | Enumerable.RightJoin Queryable.RightJoin |
注
この記事の次の例では、この領域の共通データ ソースを使用します。
各 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 回出現する結果セットを生成します。 最初のコレクション内の要素に一致する要素が存在しない場合、その要素は結果セットには表示されません。 C# の Join 句が呼び出す 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}");
}
結果は、join 句を指定せずに into 句を使用して内部結合を実行することによって取得された結果セットと同じです。 次のコードは、この同等のクエリを示しています。
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}");
}
チェーンを回避するには、次に示すように、単一の 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 番目のコレクションの相関関係を持つ要素のセットを組み合わせたものです。
注
最初のコレクションの各要素は、関連付けられた要素が 2 番目のコレクションに見つかったかどうかに関係なく、グループ結合の結果セットに表示されます。 相関要素が見つからない場合、その要素の相関要素のシーケンスは空になります。 そのため、結果セレクターは最初のコレクションのすべての要素にアクセスできます。 この動作は、2 番目のコレクションに一致しない最初のコレクションの要素にアクセスできない非グループ結合の結果セレクターとは異なります。
警告
Enumerable.GroupJoin には、従来のリレーショナル データベースの用語に直接相当するものはありません。 ただし、このメソッドでは内部結合と左外部結合のスーパーセットが実装されます。 これらの操作はどちらも、グループ化結合の観点から記述できます。 詳しくは、「Entity Framework Core、GroupJoin」をご覧ください。
この記事の最初の例では、グループ結合を実行する方法を示します。 2 番目の例は、グループ結合を使用して XML 要素を作成する方法を示しています。
グループ結合
次の例では、Department プロパティに一致する Student に基づいて、Department.ID 型と Student.DepartmentID のオブジェクトのグループ結合を実行します。 一致ごとに要素のペアを生成する非グループ結合とは異なり、グループ結合では、最初のコレクションの要素ごとに結果のオブジェクトが 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);
外部結合の実行
.NET 10 には、LeftJoin クラスとRightJoin クラスのSystem.Linq.EnumerableメソッドとSystem.Linq.Queryable メソッドが含まれています。 これらのメソッドは、 外側の左等結合と 外側の右等結合をそれぞれ実行します。 1つのシーケンスのすべてのメンバーが、たとえ2つ目のシーケンスに対応するものが見つからなくとも、出力シーケンスに含まれる結合を外側左等結合といいます。 外側の右等結合は、最初のシーケンスに一致が含まれていない場合でも、2 番目のシーケンスのすべてのメンバーが出力シーケンスに含まれる結合です。
左外部結合をエミュレートする
.NET 10 より前では、LINQ を使用して、グループ結合の結果に対して DefaultIfEmpty メソッドを呼び出して左外部結合を実行します。
次の例では、グループ結合の結果に対して DefaultIfEmpty メソッドを使用して左外部結合を実行する方法を示します。
2 つのコレクションの左外部結合を生成する最初の手順は、グループ結合を使用して内部結合を実行することです。 (このプロセスの詳細については、「内部結合の実行」参照してください。)この例ではDepartment オブジェクトのリストが、学生のStudent に一致する Department オブジェクトの ID に基づいて、DepartmentID オブジェクトのリストに内部結合されています。
2 つ目のステップは、最初 (左側) のコレクションの各要素を結果セットに含めることです。このとき、その要素と一致するものが右のコレクションにあるかどうかは考慮しません。 この手順を実行するには、グループ結合から一致する要素の各シーケンスに対して DefaultIfEmpty を呼び出します。 この例では、一致するDefaultIfEmpty オブジェクトのシーケンスごとにStudentを呼び出します。 このメソッドは、任意の Student オブジェクトに対して一致する Department オブジェクトのシーケンスが空である場合に、単一の既定値を含むコレクションを返します。これにより、結果コレクション内に各 Department オブジェクトが表されることが保証されます。
注
参照型の既定値は 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 ?? string.Empty
});
foreach (var v in query)
{
Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}
関連項目
.NET