前回は、DbContext に共通ルールを集約する方法を見ました。
今回は、マイグレーションでテーブル以外のデータベースオブジェクトを管理します。
EF Core のマイグレーションというと、テーブル作成や列追加の印象が強いかもしれません。
しかし実務では、ビュー、関数、ストアドプロシージャ、インデックスなども一緒に管理したくなります。
そのような場合は、マイグレーションの中で SQL を実行します。
SQL を直接実行する
マイグレーションには、Up() と Down() があります。
Up() は変更を進める処理、Down() は変更を戻す処理です。
public partial class AddCarSummaryView : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
CREATE VIEW dbo.CarSummaries
AS
SELECT
c.Id,
c.Make,
c.Color,
c.PetName,
COUNT(o.Id) AS OrderCount
FROM dbo.Inventory AS c
LEFT JOIN dbo.Orders AS o ON c.Id = o.CarId
GROUP BY c.Id, c.Make, c.Color, c.PetName;
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP VIEW IF EXISTS dbo.CarSummaries;
""");
}
}
これで、マイグレーション適用時にビューが作成されます。
戻すときにはビューが削除されます。
作成前に削除する
開発中は、同じビューを作り直したいことがあります。
その場合、先に削除してから作ると再実行しやすくなります。
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP VIEW IF EXISTS dbo.CarSummaries;
""");
migrationBuilder.Sql("""
CREATE VIEW dbo.CarSummaries
AS
SELECT
c.Id,
c.Make,
c.Color,
COUNT(o.Id) AS OrderCount
FROM dbo.Inventory AS c
LEFT JOIN dbo.Orders AS o ON c.Id = o.CarId
GROUP BY c.Id, c.Make, c.Color;
""");
}
ただし、本番環境では依存関係に注意が必要です。
ビューを一度削除すると、そのビューに依存する権限や別オブジェクトへ影響することがあります。
変更内容によっては、CREATE OR ALTER VIEW のような構文を使う方がよい場合もあります。
関数を作成する
データベース関数も同じように作れます。
public partial class AddCarDisplayNameFunction : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
CREATE OR ALTER FUNCTION dbo.GetCarDisplayName
(
@make nvarchar(50),
@color nvarchar(50),
@petName nvarchar(50)
)
RETURNS nvarchar(200)
AS
BEGIN
RETURN @make + N' ' + @color + N' ' + @petName;
END
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP FUNCTION IF EXISTS dbo.GetCarDisplayName;
""");
}
}
この関数は、前回見たように DbContext 側で C# メソッドと対応付けられます。
マイグレーションで関数を作り、DbContext で使えるようにする。
この 2 つをそろえることで、データベース側の処理をアプリケーションから安全に呼び出せます。
SQL をヘルパーへ分ける
マイグレーションに長い SQL を直接書くと、読みづらくなることがあります。
小さなヘルパーを用意すると整理できます。
public static class MigrationSql
{
public static void CreateOrAlterCarSummaryView(
this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
CREATE OR ALTER VIEW dbo.CarSummaries
AS
SELECT
c.Id,
c.Make,
c.Color,
c.PetName,
COUNT(o.Id) AS OrderCount
FROM dbo.Inventory AS c
LEFT JOIN dbo.Orders AS o ON c.Id = o.CarId
GROUP BY c.Id, c.Make, c.Color, c.PetName;
""");
}
public static void DropCarSummaryView(
this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP VIEW IF EXISTS dbo.CarSummaries;
""");
}
}
マイグレーション側は短くなります。
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateOrAlterCarSummaryView();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCarSummaryView();
}
ただし、ヘルパー化には注意もあります。
過去のマイグレーションが、後から変更されたヘルパーの内容に影響される可能性があるためです。
履歴として固定したい SQL は、マイグレーション内に残した方が安全な場合もあります。
インデックスを追加する
インデックスは EF Core の API でも作れます。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasIndex(car => new { car.Make, car.Color });
}
マイグレーションには、次のようなコードが生成されます。
migrationBuilder.CreateIndex(
name: "IX_Inventory_Make_Color",
table: "Inventory",
columns: new[] { "Make", "Color" });
データの検索条件が明確なら、インデックスは性能に大きく影響します。
ただし、追加しすぎると書き込みが重くなります。
問い合わせの実行計画や実際のデータ量を見ながら、必要なものを追加していきます。
適用前に SQL を確認する
マイグレーションを本番に適用する前に、SQL スクリプトを生成して確認できます。
dotnet ef migrations script
特定の範囲だけを出力することもできます。
dotnet ef migrations script PreviousMigration CurrentMigration
運用環境では、アプリケーションから自動適用するより、生成された SQL をレビューしてから適用する方が安心な場合があります。
特に注意したいのは、次のような変更です。
- 列の削除
- 型の変更
- 既存データに合わない制約追加
- 大きなテーブルへのインデックス追加
- ビューや関数の削除
マイグレーションは便利ですが、データベースへの変更であることに変わりはありません。
Down を書く意味
Down() は、変更を戻すための処理です。
開発中にはよく使います。
dotnet ef database update PreviousMigration
ただし、本番環境で安易に戻せるとは限りません。
列を削除した後に戻しても、消えたデータは復元されません。
Down() は「完全な復元保証」ではなく、「スキーマ変更を逆向きにできるようにする手段」と考える方が現実的です。
重要な変更では、バックアップ、移行スクリプト、リリース手順を合わせて考えます。
この記事のまとめ
EF Core のマイグレーションでは、テーブルだけでなく、ビュー、関数、インデックスなども管理できます。
migrationBuilder.Sql() を使えば自由度は高くなります。
その分、データベース固有の構文、依存関係、戻し方、運用手順も意識する必要があります。
データアクセス層を育てるということは、C# のコードだけでなく、データベース側のオブジェクトも同じ流れで管理できるようにすることです。
次回は、履歴テーブルを扱うリポジトリを作り、過去の状態を問い合わせる方法を見ていきます。