何があった?
.NET 10(EF Core 10)では、LINQにLeftJoin
メソッド、RightJoin
メソッドが新しく追加され、LEFT JOIN(左外部結合)、RIGHT JOIN(右外部結合)をより直感的・簡潔に書けるようになりました。
LeftJoin
は .NET 10 SDK において、System.Linq
名前空間内に追加されたLINQの拡張メソッドであり、既存のLINQクエリ式の構文を拡張して、左外部結合(LEFT JOIN)を簡潔に表現可能にするものです。
以下では、LeftJoin
メソッドについて解説します。
.NET 10 以前の書き方
.NET 10以前では、LEFT JOINを表現するには、以下のように、GroupJoin
、SelectMany
、DefaultIfEmpty
を組み合わせる必要がありました。
コードが長くて読みにくい。
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 ?? "[NONE]"
});
.NET 10 ではどうなる?
.NET 10 からは、以下のように LeftJoin
を使って書けます。シンプルで読みやすい!
var query = students
.LeftJoin(
departments,
student => student.DepartmentID,
department => department.ID,
(student, department) => new
{
student.FirstName,
student.LastName,
Department = department.Name ?? "[NONE]"
});
LeftJoin
メソッドのシグネチャ
public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner?, TResult> resultSelector,
IEqualityComparer<TKey>? comparer = null)
SQL変換について
上記のコードをEF Core(v10.0 以降)を通して実行すると、生成されるSQLは次のようになります。
SELECT s.FirstName, s.LastName,
COALESCE(d.Name, '[NONE]') AS Department
FROM Students AS s
LEFT JOIN Departments AS d ON s.DepartmentID = d.ID
LINQがそのまま自然にSQLにマッピングされるので、効率的かつ直感的なコードで実行可能なSQLを構築できます。シンプル故にミスが紛れ込みにくくなるのは良いですね。
パフォーマンス面は?
.GroupJoin(...).SelectMany(...DefaultIfEmpty())
を使ったパターンでも、
.LeftJoin(...)
を使ったパターンでも、正しく書けていれば生成されるSQLは同じなので、パフォーマンスに差はありません。
ただし、GroupJoin
を使った書き方は構造が複雑なため、記述ミスが入りやすく、意図しないSQLが生成されてしまうリスクが高まります。
その結果、パフォーマンスが低下する可能性も高くなります。
例1. DefaultIfEmpty()のつけ忘れ
var query = context.Students
.GroupJoin(
context.Departments,
student => student.DepartmentID,
department => department.ID,
(student, departments) => new { student, departments })
.SelectMany(
x => x.departments, // ← `DefaultIfEmpty()` をつけ忘れている!
(x, department) => new
{
x.student.FirstName,
Department = department.Name
});
問題
- LEFT JOIN ではなく INNER JOIN 相当のクエリになる
- 部署がない学生は結果から除外される(ビジネスロジック的にもNG)
SELECT s.FirstName, d.Name
FROM Students AS s
JOIN Departments AS d ON s.DepartmentID = d.ID
例2. 匿名型を深くネストしてしまう
var query = context.Students
.GroupJoin(
context.Departments,
student => student.DepartmentID,
department => department.ID,
(student, departments) => new
{
Nested = new { Student = student, Departments = departments }
})
.SelectMany(
x => x.Nested.Departments.DefaultIfEmpty(),
(x, department) => new
{
x.Nested.Student.FirstName,
Department = department != null ? department.Name : "[NONE]"
});
問題
- EF Coreがうまく最適化できず、OUTER APPLY を含む複雑で遅いSQLに変換されることがある
SELECT ...
FROM Students AS s
OUTER APPLY (
SELECT ...
FROM Departments AS d
WHERE s.DepartmentID = d.ID
) AS d