EntityFrameworkCore で SQLite3 を使ってみる（その３）

[EntityFrameworkCore で SQLite3 を使ってみる（その２）](https://qiita.com/yumimatoba14/items/422b93ef9e7ab2b2658b) の続きです。
いくつか雑多なメモを記録します。

ソースコードは [GitHub](https://github.com/yumimatoba14/SQLiteTest) で公開しています。
git リポジトリ：https://github.com/yumimatoba14/SQLiteTest.git

# 3. 削除時の Cascade 動作について

DbContext.Remove() でエンティティの削除が可能ですが、その時の動作が望ましくなかったので調整しました。（Task の削除は単純なインスタンスの削除ではないものとしたく、TaskRelation をカスケードに消す動作にはしたくない。）

1. 「[SHA-1: 80f4af02d6345c5bf547cddbd73e583132a24530](https://github.com/yumimatoba14/SQLiteTest/commit/80f4af02d6345c5bf547cddbd73e583132a24530)」においてはカスケードな削除が行われます
2. 「[SHA-1: 6a4d90593543f7837ac268d61e04a29f42770bba](https://github.com/yumimatoba14/SQLiteTest/commit/6a4d90593543f7837ac268d61e04a29f42770bba)」において [DeleteBehavior][DeleteBehavior] を Restrict に変更しました。
   Task の削除のコードで以下の例外が発生します
   > ハンドルされていない例外: System.InvalidOperationException: The association between entity types 'Task' and 'TaskRelation' has been severed but the relationship is either marked as 'Required' or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, then setup the relationship to use cascade deletes.
3. 「[SHA-1: 742e83d13c970d6723c1a6dc5a43d27350e05054](https://github.com/yumimatoba14/SQLiteTest/commit/742e83d13c970d6723c1a6dc5a43d27350e05054)」のように実装すれば削除可能（確認のために実装）

必須のナビゲーションプロパティの場合既定で DeleteBehavior.Cascade が初期値らしいです。（カスケードな削除が行われる。） 上記 1. の状態が該当します。
他方 null許容なナビゲーションプロパティの場合は既定で DeleteBehavior.ClientSetNull らしいです。（nullが代入される。） ClientSetNull と SetNull の違いは不明です。 なんとなくクライアント側で実行するかDB側で実行するかの違いかなと予想できますが、未確認。
今回は DeleteBehavior.Restrict を用いましたが、 NoAction との違いは分かりません。

なお SQLite では DeleteBehavior の値をマイグレーションで変更することはできないようです。 そのため 2. ではデータベースを作り直しました。

# 4. コードからのMigrationの実行

https://qiita.com/minoru-nagasawa/items/961f6eae809a379c1b52

を見て「コードからもマイグレーションできるんだ、やってみよ」と思ったのですが...
[DbMigrator](https://learn.microsoft.com/ja-jp/dotnet/api/system.data.entity.migrations.dbmigrator?view=entity-framework-6.2.0) クラスは EntityFrameworkCore にはなし、[IMigrator][IMigrator] というインタフェースがあることは分かったのですが調べた限りで実装クラスが見当たらず。
EntityFrameworkCore では DbContext.Database.[Migrate()][MigrateMethod] メソッドを呼べばよいようです。

コードは[以下のコミット](https://github.com/yumimatoba14/SQLiteTest/commit/cf0dc422f9b2df750a4a4f73e305c9854d09ec64)を参照してください。
> Revision: cf0dc422f9b2df750a4a4f73e305c9854d09ec64
Message:
modified to execute migration in code.

# 5. マイグレーションのリバート

Update-Database の引数 -Migration にマイグレーション名を指定することでその時点にリバートすることもできるようです。

```
PM> Update-Database -Migration AddTaskRelation2
```

データベースをリバートしておかないと、Remove-Migration が動かなかったりします。ただ

> System.NotSupportedException: SQLite does not support this migration operation ('DropColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262.

とエラーが出ることもあり。動かしてみないと実際にリバートできるかどうかは不明なようです。

# 6. マイグレーション用の DDL の書き出し

パッケージマネージャーコンソールで [Script-Migration](https://learn.microsoft.com/ja-jp/ef/core/cli/powershell#script-migration) を実行すればマイグレーション用の SQL を出力できるようです。

```
PM> Script-Migration -From InitialCreate -Output migration.sql
```

```SQL:migration.sql
CREATE TABLE "Tasks" (
    "WorkflowId" INTEGER NOT NULL,
    "SubId" INTEGER NOT NULL,
    "Name" TEXT NOT NULL,
    "Remark" TEXT NULL,
    CONSTRAINT "PK_Tasks" PRIMARY KEY ("WorkflowId", "SubId"),
    CONSTRAINT "FK_Tasks_Workflows_WorkflowId" FOREIGN KEY ("WorkflowId") REFERENCES "Workflows" ("Id") ON DELETE CASCADE
);

CREATE UNIQUE INDEX "IX_Workflows_Id" ON "Workflows" ("Id");

CREATE INDEX "IX_Tasks_WorkflowId_SubId" ON "Tasks" ("WorkflowId", "SubId");

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20250428110355_AddTask', '3.1.32');

DROP INDEX "IX_Tasks_WorkflowId_SubId";

CREATE TABLE "TaskRelations" (
    "WorkflowId" INTEGER NOT NULL,
    "PrevTaskSubId" INTEGER NOT NULL,
    "NextTaskSubId" INTEGER NOT NULL,
    CONSTRAINT "PK_TaskRelations" PRIMARY KEY ("WorkflowId", "PrevTaskSubId", "NextTaskSubId"),
    CONSTRAINT "FK_TaskRelations_Tasks_WorkflowId_NextTaskSubId" FOREIGN KEY ("WorkflowId", "NextTaskSubId") REFERENCES "Tasks" ("WorkflowId", "SubId") ON DELETE RESTRICT,
    CONSTRAINT "FK_TaskRelations_Tasks_WorkflowId_PrevTaskSubId" FOREIGN KEY ("WorkflowId", "PrevTaskSubId") REFERENCES "Tasks" ("WorkflowId", "SubId") ON DELETE RESTRICT
);

CREATE UNIQUE INDEX "IX_Tasks_WorkflowId_SubId" ON "Tasks" ("WorkflowId", "SubId");

CREATE INDEX "IX_TaskRelations_WorkflowId_NextTaskSubId" ON "TaskRelations" ("WorkflowId", "NextTaskSubId");

CREATE UNIQUE INDEX "IX_TaskRelations_WorkflowId_PrevTaskSubId_NextTaskSubId" ON "TaskRelations" ("WorkflowId", "PrevTaskSubId", "NextTaskSubId");

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20250430163634_AddTaskRelation2', '3.1.32');
```

__EFMigrationsHistory へのレコードの追加も書かれているので、どこまで適用しているかも分かりやすいですね。
Script-DbContext というコマンドもあり DDL 全体が出力されますが、 __EFMigrationsHistory が追加されないのはちょっと微妙。使い方に迷うところ。

# 参照ページ

https://learn.microsoft.com/ja-jp/ef/core/cli/powershell

[DeleteBehavior]: https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.entityframeworkcore.deletebehavior?view=efcore-3.1&devlangs=csharp&f1url=%3FappId%3DDev16IDEF1%26l%3DJA-JP%26k%3Dk(Microsoft.EntityFrameworkCore.DeleteBehavior.Restrict)%3Bk(TargetFrameworkMoniker-.NETFramework%2CVersion%3Dv4.7.2)%3Bk(DevLang-csharp)%26rd%3Dtrue
[IMigrator]: https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.entityframeworkcore.migrations.imigrator?view=efcore-3.1
[MigrateMethod]: https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.migrate?view=efcore-3.1#microsoft-entityframeworkcore-relationaldatabasefacadeextensions-migrate(microsoft-entityframeworkcore-infrastructure-databasefacade)