EF Core 9 の新機能: 知っておくべき機能

· 6分で読める

Entity Framework Core 9 は、2024 年 11 月に .NET 9 とともに出荷されました。いくつかのプロジェクトにわたって Entity Framework Core 9 の作業に十分な時間を費やした後、これはここしばらくで最も意味のあるリリースの 1 つであると言えます。車輪の再発明ではなく、クエリ変換、パフォーマンス、最新のデータ パターンの操作など、これまで EF Core が最も摩擦を引き起こしてきた領域を磨き上げるためです。

この投稿では、私の日常の仕事に最も大きな影響を与えた機能について説明します。まだ EF Core 8 (または 7) を使用している場合、これにより、アップグレードの向こう側で何が待っているかが明確にわかるはずです。

.NET 9 エコシステムの EF Core 9

EF Core 9 は .NET 8 および .NET 9 をターゲットとしています。つまり、これらの機能のほとんどを利用するためにアプリケーション全体を .NET 9 にアップグレードする必要は必ずしもありません。とはいえ、AOT とパフォーマンスの向上の一部は .NET 9 ランタイムの変更と密接に関係しているため、最後まで実行することで最大限の効果を得ることができます。

このリリースは、Microsoft が確立した奇数/偶数のペースに従います。奇数番号のリリース (.NET 9 など) は 18 か月のサポートが付いた標準期間サポート (STS) であり、偶数のリリース (.NET 8 など) は長期サポート (LTS) です。アップグレードのスケジュールを計画するときは、この点に留意してください。

LINQ 変換の改善

これは、ほとんどの開発者がすぐに違いを感じる点です。 EF Core 9 は、LINQ 式を実際に意味のある SQL に変換する点で大幅な進歩を遂げました。

GroupBy 翻訳の改善

EF Core で GroupBy クエリを作成し、最終的にクライアント側の評価警告や奇妙な SQL が発生したことがある方なら、その苦しみをご存知でしょう。 EF Core 9 は、より広範な GroupBy シナリオのセットを SQL で直接処理します。

var salesByCategory = await context.Products
    .GroupBy(p => p.Category.Name)
    .Select(g => new
    {
        Category = g.Key,
        TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
        AveragePrice = g.Average(p => p.Price),
        ProductCount = g.Count()
    })
    .OrderByDescending(x => x.TotalRevenue)
    .ToListAsync();

以前のバージョンでは、GroupBy 内のナビゲーション プロパティに対する集計を含むクエリは、クライアント評価にフォールバックすることがありました。 EF Core 9 は、これを GROUP BYSUMAVG、および COUNT を使用した単一の SQL クエリにきれいに変換します。

複雑なプロジェクションとサブクエリ

ネストされたサブクエリと複雑なプロジェクションも大幅にアップグレードされました。次のようなことを考えてみましょう。

var orderSummaries = await context.Customers
    .Select(c => new CustomerSummaryDto
    {
        Name = c.FullName,
        TotalOrders = c.Orders.Count(),
        MostRecentOrder = c.Orders
            .OrderByDescending(o => o.OrderDate)
            .Select(o => new OrderBriefDto
            {
                Id = o.Id,
                Date = o.OrderDate,
                Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
            })
            .FirstOrDefault(),
        TopCategory = c.Orders
            .SelectMany(o => o.LineItems)
            .GroupBy(li => li.Product.Category.Name)
            .OrderByDescending(g => g.Count())
            .Select(g => g.Key)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 では、クライアント側の評価をトリガーせずに、この式全体を SQL に変換できるようになりました。生成されたクエリでは、必要に応じて相関サブクエリとラテラル結合が使用され、SQL プランは以前のバージョンで生成されたものよりも大幅に効率的です。

パラメータ化されたプリミティブ コレクション

LINQ の顕著な改善点の 1 つは、プリミティブ値のコレクションをクエリに直接渡す機能です。

var statusFilter = new List<string> { "Active", "Pending", "Review" };

var filteredOrders = await context.Orders
    .Where(o => statusFilter.Contains(o.Status))
    .ToListAsync();

EF Core 8 では、これはインライン化された値を持つ IN 句を使用して変換されました。つまり、リストが変更されたときにクエリ プラン キャッシュを再利用できませんでした。 EF Core 9 は、これらのコレクションを適切にパラメーター化し、構造化パラメーターとして送信します。これは、SQL Server および PostgreSQL のクエリ プラン キャッシュにとって大きな問題です。## 一括操作 - ExecuteUpdate および ExecuteDelete

ExecuteUpdateExecuteDelete は EF Core 7 で導入されましたが、EF Core 9 では、それらを使用してできることが有意義な方法で拡張されています。

より複雑な更新式

ExecuteUpdate で、ナビゲーション プロパティを介した他のテーブルへの参照など、より複雑な式を使用できるようになりました。

await context.Products
    .Where(p => p.Category.IsDiscontinued)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsAvailable, false)
        .SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
        .SetProperty(p => p.Price, p => p.Price * 0.5m));

これにより、JOIN を含む単一の UPDATE ステートメントがカテゴリ テーブルに生成されます。エンティティをメモリにロードする必要はなく、変更追跡のオーバーヘッドもありません。

サブクエリを使用した条件付き一括削除

サブクエリ フィルターを使用した一括削除が完全にサポートされるようになりました。

await context.AuditLogs
    .Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
    .Where(log => !context.ProtectedRecords
        .Any(pr => pr.AuditLogId == log.Id))
    .ExecuteDeleteAsync();

これは、NOT EXISTS サブクエリを含む DELETE に変換され、まさに手動で記述するものとなります。エンティティがロードされておらず、往復もありません。

JSON 列の機能強化

JSON 列は、最近の EF Core リリースで最も魅力的な機能の 1 つであり、EF Core 9 ではさらに進化しています。

JSON 内でのクエリ

より優れた翻訳サポートにより、JSON 列内からデータをフィルターして投影できるようになりました。

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public ShippingAddress Address { get; set; } // Stored as JSON
    public List<OrderNote> Notes { get; set; }   // Stored as JSON
}

public class ShippingAddress
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public class OrderNote
{
    public DateTime CreatedAt { get; set; }
    public string Text { get; set; }
    public string Author { get; set; }
}

DbContext の構成:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>(builder =>
    {
        builder.OwnsOne(o => o.Address, ab =>
        {
            ab.ToJson();
        });

        builder.OwnsMany(o => o.Notes, nb =>
        {
            nb.ToJson();
        });
    });
}

これで、JSON に直接クエリできるようになりました。

var newYorkOrders = await context.Orders
    .Where(o => o.Address.State == "NY")
    .OrderByDescending(o => o.Notes.Count)
    .Select(o => new
    {
        o.CustomerName,
        o.Address.City,
        LatestNote = o.Notes
            .OrderByDescending(n => n.CreatedAt)
            .Select(n => n.Text)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 は、SQL Server (または他のプロバイダーの同等のもの) 上で適切な JSON_VALUE および JSON_QUERY 呼び出しを生成し、その変換では、以前よりもはるかに広範囲の JSON 要素に対する LINQ 操作がカバーされます。

JSON プロパティの更新

EF Core 8 の問題点の 1 つは、JSON 列内の 1 つのプロパティを更新すると、JSON ドキュメント全体が書き換えられることです。 EF Core 9 では、JSON マップ型のより詳細な変更追跡によりこれが改善され、可能な場合はよりターゲットを絞った更新が生成されます。

var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();

サポートされているプロバイダーでは、BLOB 全体を書き換えるのではなく、よりターゲットを絞った JSON 変更を生成できます。

複合型 - ID のない値オブジェクト

複合型は、ドメイン駆動設計の実践者が待ち望んでいた機能の 1 つです。所有型とは異なり、複合型にはアイデンティティがなく、純粋な値のオブジェクトです。

[ComplexType]
public record Money(decimal Amount, string Currency);

[ComplexType]
public record DateRange(DateTime Start, DateTime End);

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Money Budget { get; set; }
    public DateRange Timeline { get; set; }
}

これらは、別のテーブルやいかなる種類のキーも必要とせずに、親テーブル (Budget_AmountBudget_CurrencyTimeline_StartTimeline_End) にフラット化された列として格納されます。

所有型との主な違い: 複合型は参照ではなく値で比較されます。同じ Amount および Currency を持つ 2 つの Money インスタンスは、どのエンティティに属しているかに関係なく、等しいとみなされます。

var expensiveProjects = await context.Projects
    .Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
    .OrderByDescending(p => p.Budget.Amount)
    .ToListAsync();

これは、フラット化された列のフィルタリングに直接変換されます。クリーンで効率的で、まさに期待どおりの結果になります。

SQL Server の HierarchyId サポート

SQL Server の階層データ (組織図、カテゴリ ツリー、ファイル システム) を扱ったことがある場合は、HierarchyId がこのための組み込み型であることをご存知でしょう。 EF Core 9 は、EF Core 9 に対する最上級のサポートを提供します。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public HierarchyId PathFromCeo { get; set; }
}

階層関係を直接クエリできるようになりました。

var managerId = HierarchyId.Parse("/1/3/");

// All direct and indirect reports
var allReports = await context.Employees
    .Where(e => e.PathFromCeo.IsDescendantOf(managerId))
    .Where(e => e.PathFromCeo != managerId) // exclude the manager
    .OrderBy(e => e.PathFromCeo)
    .ToListAsync();

// Direct reports only (one level down)
var directReports = await context.Employees
    .Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
    .ToListAsync();

// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
    .Select(e => new
    {
        e.Name,
        e.Title,
        Level = e.PathFromCeo.GetLevel()
    })
    .OrderBy(e => e.Level)
    .ToListAsync();
```これらはすべて、SQL Server のネイティブ `HierarchyId` メソッドに変換されます。自己参照外部キーと再帰 CTE を使用してツリー構造を実装している場合、これはよりクリーンなアプローチです。

## コンパイルされたモデルと AOT のサポート

パフォーマンスを重視する開発者は、コンパイルされたモデルと事前 (AOT) コンパイルのサポートへの継続的な投資を高く評価するでしょう。

### コンパイルされたモデル

コンパイルされたモデルは、EF Core が通常起動時に構築するモデル メタデータを事前に生成します。大規模なモデル (数百のエンティティを想定) の場合、これによりコールド スタート時間を大幅に短縮できます。

```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels

次に、配線します。

builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(connectionString)
        .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));

EF Core 9 では、コンパイルされたモデルがより完全になり、より多くのマッピング機能がサポートされ、より小さな出力が生成されます。約 400 のエンティティを含むモデルの場合、起動時間は数秒からほぼ瞬時に短縮される可能性があります。

AOT コンパイルの進行状況

EF Core の完全なネイティブ AOT サポートはまだ進行中ですが、EF Core 9 は大幅に進歩しています。リフレクションの多いコード パスの多くはトリミングしやすいようにリファクタリングされており、コンパイルされたモデルは AOT ストーリーの重要な部分です。 Azure Functions やマイクロサービスなど、コールド スタートが重要なシナリオをターゲットにしている場合、これらの改善は直接関係します。

Cosmos DB プロバイダーの更新

Azure Cosmos DB プロバイダーは、EF Core 9 で成熟し続けています。いくつかの注目すべき改善点は次のとおりです。

パーティションキーの処理

プロバイダーは階層型パーティション キーをサポートし、パーティション キー フィルターをよりインテリジェントに処理するようになりました。

public class TenantDocument
{
    public string Id { get; set; }
    public string TenantId { get; set; }  // Partition key
    public string Region { get; set; }     // Sub-partition key
    public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
    .Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
    .Where(d => d.Content.Contains("important"))
    .ToListAsync();

LINQ から NoSQL への変換の改善

ContainsAny、入れ子になった配列操作、数学関数のサポートの強化など、より多くの LINQ 操作が Cosmos DB の SQL 言語に変換されるようになりました。以前はクライアント評価にフォールバックされていたクエリがサーバー側で処理されるようになりました。

ベクトル検索のサポート

EF Core 9 では、Cosmos DB によるベクトル類似性検索の早期サポートが導入されています。これは、埋め込みや AI 駆動の検索と統合するアプリケーションを構築する場合に役立ちます。

var results = await context.Documents
    .OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
    .Take(10)
    .ToListAsync();

移行の改善

移行により生活の質が向上し、チーム環境での作業の苦痛が軽減されました。

移行におけるテンポラル テーブル

期間列と履歴テーブルの名前付けが適切にサポートされ、移行でテンポラル テーブルの構成がより適切に処理されるようになりました。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .ToTable("Employees", b => b.IsTemporal(t =>
        {
            t.HasPeriodStart("ValidFrom");
            t.HasPeriodEnd("ValidTo");
            t.UseHistoryTable("EmployeeHistory");
        }));
}

冪等スクリプト

Script-Migration コマンド (およびそれに相当する CLI) は、デフォルトでより適切な冪等スクリプトを生成し、特定の状態に存在するデータに依存するスキーマ変更に関するエッジ ケースの処理が改善されています。

移行バンドル

移行をデプロイ用のスタンドアロン実行可能ファイルにパッケージ化する移行バンドルは、EF Core 9 ではより優れたエラー報告と一時的な障害に対する再試行ロジックにより信頼性が向上しています。

dotnet ef migrations bundle --self-contained -r linux-x64

これにより、デプロイメント ターゲットに .NET SDK をインストールしなくても、CI/CD パイプラインで実行できるバイナリが生成されます。

パフォーマンスのベンチマーク以下は私自身のテストによる大まかなベンチマークです。これらは、SQL Server 2022 に対して実行されている約 200 のエンティティを含むプロジェクトからのもので、BenchmarkDotNet で測定されました。数値は異なりますが、相対的な改善は同様であるはずです。

シナリオEFコア8EFコア9改善
モデルのビルド (コールド スタート)1,850ミリ秒320ミリ秒~5.8 倍高速 (コンパイル)
単純なクエリ (PK による単一エンティティ)0.42ミリ秒0.38ミリ秒~10% 高速
複雑なクエリ (結合 + 集計)3.1ミリ秒2.4ミリ秒~23% 高速
一括更新 (10,000 行)145ミリ秒118ミリ秒~19% 高速
JSON 列クエリ2.8ミリ秒1.9ミリ秒~32% 高速
SaveChanges (100 エンティティ)48ミリ秒41ミリ秒~15% 高速

コンパイルされたモデルの改善は最も劇的ですが、特に 1 秒あたり数千のクエリを実行する高スループットのシナリオでは、全体的な着実な改善が積み重なっていきます。

EF Core 8 からのアップグレード

EF Core 8 を使用している場合、アップグレード パスは比較的スムーズです。チェックリストは次のとおりです。

1.パッケージを更新します:

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />

2.重大な変更がないか確認してください。 EF Core 9 のリストは、以前のリリースに比べて比較的短いです。最も注目すべきもの:

  • 以前に廃止された API の一部が削除されました
  • 特定の GroupBy クエリの変換方法の変更 (クエリはサーバー側で行われるようになり、クライアントの評価に依存していた場合の動作が変わります)
  • 移行スキャフォールディングの出力の小さな変更

3.コンパイル済みモデルを使用している場合は、再生成します。形式が変更されたため、古いコンパイル済みモデルは EF Core 9 では動作しなくなります。

4.テスト スイートを実行します。 以前にクライアントで評価されたクエリに特に注意してください。クエリはサーバーで評価される可能性があり、通常はその方が優れていますが、データの違いが表面化する可能性があります。

5.そのプロバイダーを使用している場合は、Cosmos DB クエリを確認してください。翻訳の改善により、一部のクエリの実行方法が異なります (通常は高速になります)。ただし、結果が同一であることを確認することは価値があります。

一般的なプロジェクトの最小限のアップグレードは次のようになります。

dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test

すべてがコンパイルされ、テストに合格した場合は、おそらく良好な状態になっています。問題が発生した場合は、EF Core 9 の重大な変更に関するドキュメントに各変更の詳細な移行ガイダンスが記載されています。

まとめ

EF Core 9 は革新的なリリースではありません。これは進化的なリリースであり、まさにそれが必要なものでした。 LINQ の機能強化だけでも、ほとんどのプロジェクトのアップグレードが正当化され、JSON 列の機能強化、複合型、HierarchyId サポートなどの機能により、以前は扱いにくかった、または不可能だったパターンが開かれます。

私のプロジェクトに最も影響を与えた機能を 3 つ挙げるなら:

  1. パラメーター化されたプリミティブ コレクション — クエリ プランのキャッシュ効率が大規模に重要となるため
  2. JSON 列の改善 — ハイブリッド リレーショナル ドキュメント パターンが非常に便利であるため
  3. コンパイル済みモデル — 起動時間は開発者の生産性と展開速度に直接影響するためEF Core チームは EF Core 5 以来堅実な軌道を歩んできており、バージョン 9 もその傾向を引き継いでいます。すでに EF Core 8 を使用している場合、アップグレードは低リスクであり、大きなメリットがあります。古いものを使用している場合は、今ほどジャンプするのに最適な時期はありません。

楽しいコーディングと楽しいクエリ。