【.NET 10 Preview】EF CoreにLEFT JOIN、RIGHT JOINが追加されるらしい

# 何があった？
.NET 10(EF Core 10)では、LINQに`LeftJoin`メソッド、`RightJoin`メソッドが新しく追加され、LEFT JOIN（左外部結合）、RIGHT JOIN（右外部結合）をより直感的・簡潔に書けるようになりました。

:::note info
`LeftJoin` は .NET 10 SDK において、`System.Linq` 名前空間内に追加されたLINQの拡張メソッドであり、既存のLINQクエリ式の構文を拡張して、左外部結合（LEFT JOIN）を簡潔に表現可能にするものです。
:::

以下では、`LeftJoin`メソッドについて解説します。

# .NET 10 以前の書き方
.NET 10以前では、LEFT JOINを表現するには、以下のように、`GroupJoin`、`SelectMany`、`DefaultIfEmpty` を組み合わせる必要がありました。
コードが長くて読みにくい。
```c#
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` を使って書けます。シンプルで読みやすい！
```c#
var query = students
    .LeftJoin(
        departments,
        student => student.DepartmentID,
        department => department.ID,
        (student, department) => new 
        { 
            student.FirstName,
            student.LastName,
            Department = department.Name ?? "[NONE]"
        });
```
`LeftJoin`メソッドのシグネチャ
```c#
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は次のようになります。
```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が生成されてしまうリスクが高まります。
その結果、パフォーマンスが低下する可能性も高くなります。
<details><summary>例1. DefaultIfEmpty()のつけ忘れ</summary>

```c#
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）
```sql
SELECT s.FirstName, d.Name
FROM Students AS s
JOIN Departments AS d ON s.DepartmentID = d.ID
```

</details>
<details><summary>例2. 匿名型を深くネストしてしまう</summary>

```c#
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に変換されることがある
```sql
SELECT ...
FROM Students AS s
OUTER APPLY (
    SELECT ...
    FROM Departments AS d
    WHERE s.DepartmentID = d.ID
) AS d
```

</details>

# 参考
* <a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/efcore.md#support-for-the-net-10-leftjoin-operator">Support for the .NET 10 LeftJoin operator</a>
* <a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview2/efcore.md#support-for-the-net-10-rightjoin-operator">Support for the .NET 10 RightJoin operator</a>
* <a href="https://medium.com/%40malarsharmila/proposal-for-a-linq-leftjoin-operator-in-net-10-af74635079a6">Proposal for a LINQ LeftJoin Operator in .NET 10</a>