本文へスキップ

データベースのマイグレーション ​

第15章では EnsureCreated() を使って学習用データベースを手早く作成しました。アプリにデータが保存された後でエンティティにプロパティを追加しても、既存のテーブルは変更されません。この場合はマイグレーション(migration)が必要です。マイグレーションは、テーブル構造の変更をレビュー、コミット、実行できるコードとして記録します。

第23章には完成したサンプルがあります。次は Todos テーブルに Note 列を追加するために EF Core ツールが生成したマイグレーションファイルです。

AddTodoNote.cs
cs
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Deployment.Data.Migrations
{
    /// <inheritdoc />
    public partial class AddTodoNote : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<string>(
                name: "Note",
                table: "Todos",
                type: "TEXT",
                nullable: true);
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropColumn(
                name: "Note",
                table: "Todos");
        }
    }
}

対応するエンティティの全ファイルです。

Features/Todos/Todo.cs
cs
namespace TodoApi.Features.Todos;

public class Todo
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public bool Done { get; set; }
    public string? Note { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public List<Todo> Todos { get; set; } = [];
}

Up は更新を、Down は取り消しを表します。今回は null 許容列を追加するため、既存タスクの Note は null になります。このマイグレーションを取り消すと列全体が削除され、すでに入力したメモも失われます。スキーマを戻しても、データが復元されるわけではありません。

リポジトリのマイグレーションを実行する ​

リポジトリのルートディレクトリで次を実行します。

bash
cd samples/23-deployment
dotnet tool restore
dotnet ef migrations list
dotnet ef database update

この章ではローカルツールマニフェストで dotnet-ef のバージョンを固定しています。新しい todos-23.db に初めて更新を適用すると、InitialCreate と AddTodoNote の2つのマイグレーションが実行され、カテゴリーとタスクのテーブルが作成されて、固定のカテゴリーが2件追加されます。再度実行してもテーブルは重複して作成されません。

ツールの出力に表示されるマイグレーション名にはタイムスタンプが含まれます。完全な名前は Data/Migrations 内のファイルで確認してください。データベースの __EFMigrationsHistory テーブルには、正常に適用されたマイグレーションが記録されます。

リポジトリに含まれるマイグレーションを重複生成しない

InitialCreate と AddTodoNote はすでにサンプルにコミットされています。そのまま実行してください。エンティティの構造を再度変更した場合に限り、dotnet ef migrations add で新しいマイグレーションを生成し、ファイルをレビューしてください。

ツールはデザイン時ファクトリーを使ってコンテキストを作成するため、先に API を起動したり、本番用の認証サービスを構成したりする必要はありません。

Data/TodoDbContextFactory.cs
cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace TodoApi.Data;

// dotnet ef only needs a context; authentication and the HTTP service do not need to start.
public class TodoDbContextFactory : IDesignTimeDbContextFactory<TodoDbContext>
{
    public TodoDbContext CreateDbContext(string[] args)
    {
        var config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json").AddEnvironmentVariables().AddCommandLine(args).Build();
        var connectionString = config.GetConnectionString("Todos")
            ?? throw new InvalidOperationException("Missing ConnectionStrings:Todos configuration.");
        return new TodoDbContext(new DbContextOptionsBuilder<TodoDbContext>()
            .UseSqlite(connectionString).Options);
    }
}

ここでいうデザイン時(design time)とは、EF コマンドを実行してマイグレーションを生成または適用する段階です。通常の HTTP リクエスト処理では、アプリは引き続き依存性注入コンテナーからコンテキストを取得します。

既存データを確認する方法 ​

マイグレーションコマンドが成功しただけで、更新によってデータに影響がなかったと判断してはいけません。次の完全なテストでは、旧スキーマでテーブルを作成してタスクを挿入し、その後に列追加のマイグレーションを適用します。

Tests/MigrationTests.cs
cs
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

public class MigrationTests
{
    [Fact]
    public async Task Adding_note_keeps_existing_rows()
    {
        var ct = TestContext.Current.CancellationToken;
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync(ct);
        await using var db = new TodoDbContext(new DbContextOptionsBuilder<TodoDbContext>()
            .UseSqlite(connection).Options);
        var migrator = db.GetService<IMigrator>();

        await migrator.MigrateAsync("InitialCreate", ct);
        var title = "Created before upgrade";
        await db.Database.ExecuteSqlAsync(
            $"INSERT INTO Todos (Title, Done, CategoryId) VALUES ({title}, {false}, {1})", ct);

        await migrator.MigrateAsync(cancellationToken: ct);
        var todo = await db.Todos.SingleAsync(ct);
        Assert.Equal(title, todo.Title);
        Assert.Null(todo.Note);
        Assert.Equal(2, await db.Categories.CountAsync(ct));

        todo.Note = "Added after upgrade";
        await db.SaveChangesAsync(ct);
        await migrator.MigrateAsync(cancellationToken: ct);
        Assert.Single(await db.Todos.AsNoTracking().ToListAsync(ct));
        Assert.Equal("Added after upgrade", await db.Todos.Select(t => t.Note).SingleAsync(ct));
    }
}

この章のサンプルディレクトリから次を実行します。

bash
dotnet test --project Tests/TodoApi.Tests.csproj -c Release -p:TreatWarningsAsErrors=true

合計13件のテストがすべて成功するはずです。マイグレーションのテストでは、既存のタスクとカテゴリーが残っていること、新しい列の初期値が null であることを確認します。その後メモを書き込み、再度マイグレーションを実行しても、レコードの重複作成やメモの削除が起きないことを確認します。

データベースを変更するときの手順 ​

  1. エンティティまたはモデルの構成を変更し、新しいスキーマを表現します。
  2. 今回の変更内容を説明する名前を付けて dotnet ef migrations add を実行し、マイグレーションを生成します。
  3. Up、Down、モデルスナップショットを確認し、テスト用データベースで検証します。ツールが「旧列の削除と新列の追加」を検出した場合、生成される操作が意図した名前変更になるとは限りません。
  4. マイグレーションファイルをアプリケーションコードと一緒にコミットします。本番で実行する前にデータをバックアップし、更新のタイミングを計画します。

モデルスナップショットには前回のマイグレーション後のモデルが保存されます。ツールはその内容と現在のモデルを比較して、次の変更を生成します。不整合を「直す」ためにスナップショットを手動で削除しないでください。

第23章では独立した --migrate コマンドで更新を実行し、成功してからサービスを起動します。実際のシステムでは SQL スクリプトのレビューやマイグレーションバンドルの利用もできます。デプロイ方法やデータベースプロバイダーによって選択肢が異なります。詳しくは公式のマイグレーション適用ガイドを参照してください。

EnsureCreated を使った既存データベースの場合 ​

EnsureCreated() はマイグレーション履歴を作成しません。そのようなデータベースに InitialCreate を直接適用すると、通常はテーブルがすでに存在するため失敗します。

このチュートリアルでは、第23章に専用の新しいデータベースファイルを使うため、前の章のデータを上書きしません。古いデータベースに保持すべき情報がある場合は、まずバックアップを取り、データのインポートまたはマイグレーションのベースライン作成を計画して、複製データベースで練習してください。古いデータベースを削除したり、マイグレーション履歴を偽造したりして、この手順を飛ばしてはいけません。それぞれの適用範囲は公式のデータベース作成に関する説明を参照してください。

まとめ ​

  • マイグレーションはテーブル構造の変更を記録します。EnsureCreated はマイグレーションを必要としないデータベースの作成に適しています。
  • モデルを変更したらマイグレーションを生成し、レビューしてから適用します。マイグレーションとスナップショットは一緒にコミットしてください。
  • 空のデータベースだけでなく、既存のレコードを使って更新を検証します。
  • null 許容列の追加なら既存の行を保てますが、列の削除などではデータが失われます。
  • 本番更新の前にバックアップを取り、アプリが新しいスキーマを使い始める前に更新を計画します。

戻る:公開とデプロイ。続けて読む:応用テーマ。

.NET 10 と Minimal API を使用 · 各章に実行可能なサンプルを用意