LINQ のJoin 操作
2 つのデータ ソースの join とは、あるデータ ソースのオブジェクトを、共通の属性を共有する別のデータ ソースのオブジェクトと関連付けることです。
重要
これらのサンプルでは、System.Collections.Generic.IEnumerable<T> データ ソースを使用します。 System.Linq.IQueryProvider に基づくデータ ソースでは、System.Linq.IQueryable<T> データ ソースと式ツリーが使用されます。 式ツリーには、許可される C# 構文に制限があります。 さらに、EF Core などの各 IQueryProvider
データ ソースでは、より多くの制限が課される場合があります。 ご利用のデータ ソースのドキュメントをご覧ください。
相互に直接の関連がない 2 つのデータ ソースを対象とするクエリにおいて、結合は重要な操作になります。 オブジェクト指向プログラミングでは、結合は一方向の関係における逆方向など、モデル化されていないオブジェクト間の相関関係を意味する場合があります。 一方向の関係の例として、専攻を表す Department
型のプロパティを持つ Student
クラスがあります。ただし、Department
クラスには、Student
オブジェクトのコレクションを表すプロパティはありません。 Department
オブジェクトのリストから各部門のすべての学生を検索する場合は、join 操作を使用して学生を検索できます。
LINQ フレームワークに用意されている join メソッドは Join と GroupJoin です。 この 2 つのメソッドは、等結合 (キーが等しいかどうかに基づいて 2 つのデータ ソースを対応させる結合) を実行します。 (比較の場合、Transact-SQL では、equals
以外の join 操作 (less than
操作など) がサポートされています)。リレーショナル データベース用語では、Join は内部 join を実装します。これは、他のデータ セットで一致するオブジェクトのみが返される join の一種です。 リレーショナル データベース用語で GroupJoin メソッドに直接相当するものはありませんが、このメソッドは内部結合と左外部結合のスーパーセットを実装します。 左外部の join とは、最初 (左側) のデータ ソースの各要素を返す join です。これらの要素は、もう一方のデータ ソースの要素と相関関係がなくても返されます。
次の図は、2 つのセットと、内部 join または左外部 join としてこれらのセットに含まれている要素の概念図を示しています。
メソッド
メソッド名 | 説明 | 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 つのシーケンスを join します。
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 つのシーケンスを join し、要素ごとに結果として得られる一致をグループ化します。
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}");
}
}
内部結合の実行
リレーショナル データベースでは、内部 join により、2 番目のコレクション内の一致するすべての要素に対して、最初のコレクションの各要素が一度表示される結果セットが生成されます。 最初のコレクション内の要素に一致する要素が存在しない場合、その要素は結果セットには表示されません。 Join メソッドは、C# の join
句によって呼び出され、内部 join を実装します。 次の例は、内部 join の 4 つのバリエーションを実行する方法を示しています。
- 簡単なキーに基づいて、2 つのデータ ソースの要素を関連付ける単純な内部 join。
- 複合 キーに基づいて、2 つのデータ ソースの要素を関連付ける内部 join。 複合キーは複数の値で構成され、複数のプロパティに基づいて要素を関連付けることができます。
- 一連の join 操作が相互に追加された 複数の join。
- グループ join を使用して実装された内部 join。
単一キー join
次の例では、Teacher
オブジェクトを、その Teacher
と一致する TeacherId
を持つ Department
オブジェクトと照合します。 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}");
}
学科長ではない教師は、最終結果には表示されません。
複合キー join
1 つのプロパティだけに基づいて要素を関連付ける代わりに、複合キーを使用して、複数のプロパティに基づいて要素を比較できます。 各コレクションに対してキー セレクター関数を指定し、比較するプロパティで構成された匿名型を返します。 プロパティにラベルを付ける場合は、各キーの匿名型に同じラベルを付ける必要があります。 また、プロパティは、同じ順序で表示する必要があります。
次の例では、Teacher
オブジェクトのリストと Student
オブジェクトのリストを使用して、学生でもある教師を調べます。 どちらの型にも、各人の姓と名を表すプロパティがあります。 それぞれのリストの要素から join キーを作成する関数は、プロパティで構成される匿名型を返します。 join 操作により、これらの複合キーが等しいかどうか比較され、それぞれのリストの氏名が一致するオブジェクトのペアが返されます。
// 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);
}
複数 join
任意の数の join 操作を相互に追加して、複数の join を実行できます。 C# の各 join
句は、指定されたデータ ソースを前の join の結果に関連付けます。
最初の join
句は、Department
オブジェクトの ID
と一致する Student
オブジェクトの DepartmentID
に基づいて学生と学科を照合します。 この操作で、Student
オブジェクトと Department
オブジェクトが含まれた匿名型のシーケンスが返されます。
2番目の join
句は、最初の join で返される匿名型を、部門長 ID と一致するその教師の ID に基づいて Teacher
オブジェクトを関連付けます。 この操作で、学生名、学科名、学科長名を含む匿名型のシーケンスが返されます。 この操作は内部 join であるため、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}".""");
}
グループ化 join を使用した内部 join
グループ join を使用して内部 join を実装する方法を次の例に示します。 Department
オブジェクトのリストは、Student.DepartmentID
プロパティと一致する Department.ID
に基づいて、Student
オブジェクトのリストにグループ結合されます。 グループ join によって、それぞれのグループが 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
句を使用して内部 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}");
}
グループ化結合の実行
グループ join は、階層データ構造を作成する場合に便利です。 これは、最初のコレクションの各要素と、2 番目のコレクションの相関関係を持つ要素のセットを組み合わせたものです。
Note
最初のコレクションの各要素は、2 番目のコレクションに相関関係を持つ要素があるかどうかにかかわらず、グループ join の結果セットに表示されます。 相関関係を持つ要素が見つからない場合、その要素の相関関係を持つ要素のシーケンスは空です。 そのため、結果セレクターは最初のコレクションのすべての要素にアクセスできます。 これは、非グループ join の結果セレクターとは異なります。非グループ結合の結果セレクターは、2 番目のコレクションに一致するものがない最初のコレクションの要素にアクセスすることはできません。
警告
Enumerable.GroupJoin には、従来のリレーショナル データベースの用語に直接相当するものはありません。 ただし、このメソッドでは内部結合と左外部結合のスーパーセットが実装されます。 これらの操作はどちらも、グループ化 join の観点から記述できます。 詳しくは、「Entity Framework Core、GroupJoin」をご覧ください。
この記事の最初の例では、グループ join を実行する方法を示します。 2 つ目の例では、グループ join を使用して XML 要素を作成する方法を示します。
グループ join
次の例では、Student.DepartmentID
プロパティと一致する Department.ID
に基づいて、Department
型と Student
型のオブジェクトのグループ join を実行します。 一致ごとに要素のペアを生成する非グループ join とは異なり、グループ join は最初のコレクションの要素ごとに 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 を作成するグループ join
グループ結合は、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);
左外部結合の実行
左外部 join は、最初のコレクションの各要素を、2 つ目のコレクション内にある要素との相関関係の有無にかかわらず返す join です。 LINQ を使用すると、グループ join の結果に対して DefaultIfEmpty メソッドを呼び出すことで、左外部 join を実行できます。
次の例は、グループ join の結果に対して DefaultIfEmpty メソッドを使用し、左外部 join を実行する方法を示しています。
2 つのコレクションの左外部 join を作成するための最初のステップは、グループ join を使用して内部 join を実行することです。 (このプロセスの詳細については、「内部結合の実行」参照してください。)この例ではDepartment
オブジェクトのリストが、学生のDepartmentID
に一致する Department
オブジェクトの ID に基づいて、Student
オブジェクトのリストに内部結合されています。
2 つ目のステップは、最初 (左側) のコレクションの各要素を結果セットに含めることです。このとき、その要素と一致するものが右のコレクションにあるかどうかは考慮しません。 これを行うには、グループ join 内の一致する要素の各シーケンスに対して、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, departmentList) => new { student, subgroup = departmentList.AsQueryable() })
.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}");
}
関連項目
.NET