//タイトルがなげぇ
環境
- Windows10 Pro (1903) 64bit
- Visual Studio 2019
- Microsoft.EntityFrameworkCore.Sqlite ver.3.0.0
要約すると
- SqliteのMigrationは一部機能が未対応で、自動生成されたMigrationクラスがそのままだと反映できない場合がある。
- SQLite does not support this migration operation ('メソッド名')みたいなエラーが出る。
- そういう変更が発生した場合は、自分でクエリを書いてUp()、Down()に組み込む必要がある。
- クエリの実行はMigrationBuilder.Sql()メソッドで出来る。
- 基本的には以下の手順でSqlを実行する。
- 新テーブル定義で別名のテーブルを作る
- 旧テーブルから新別名テーブルにデータをコピー
- 旧テーブルを削除
- 新別名テーブルのテーブル名を旧テーブルのテーブル名に変更
Migrationが失敗する…
先日の記事で作ったモデルを変更しました。
namespace EFCoreTest
{
public class Product
{
[Key]
public string ProductId { get; set; }
public int UnitPrice { get; set; }
public string Name { get; set; }
public Product()
{
ProductId = Guid.NewGuid().ToString();
UnitPrice = 0;
Name = string.Empty;
}
}
}
これを
namespace EFCoreTest
{
public class Product
{
[Key]
public string ProductId { get; set; }
[Column(TypeName = "numeric")]
public decimal UnitPrice { get; set; } //←この列の定義をint→decimalに変更
public string Name { get; set; }
public Product()
{
ProductId = Guid.NewGuid().ToString();
UnitPrice = 0;
Name = string.Empty;
}
}
}
こんな感じ。
Migration作ると
public partial class UnitPriceToDecimal : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "UnitPrice",
table: "Products",
type: "numeric",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "UnitPrice",
table: "Products",
type: "INTEGER",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric");
}
}
うんうん。AlterColumnとかそれっぽいメソッド名ついとる。
update-databaseします。
update-database
…省略…
SQLite does not support this migration operation ('AlterColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262.
まさかのエラー。
なんで…
SQLiteのMigrationは一部未対応らしい
とりあえずエラーメッセージ中のリンク先に飛んでみる。→こちら
下の方を読んでみると結構未対応なMigrationがあるようで。
Webページ[SQLite データベース プロバイダー - 制限事項 - EF Core | Microsoft Docs] https://docs.microsoft.com/ja-jp/ef/core/providers/sqlite/limitations より引用

うーん、ぱっと見、テーブルはそのままに既存の列やインデックスの情報を変更するタイプが未対応なのかな?
AlterColumnもしっかり「×」になっております。
「移行の制限の回避策」を眺めてみる
同ページ内の「以降の制限の回避策」から飛べるサイトが→こちら
Webページ[SQLite Query Language: ALTER TABLE] https://sqlite.org/lang_altertable.html#otheralter より引用

なるほど。
まぁ早い話が
- 新しいテーブル定義で別名のテーブルを作る
- 古いテーブルから新しいテーブルにデータをコピー
- 古いテーブルを削除
- 別名だったテーブルの名前を古いテーブル名に変更
っていう手順を手動でやれってことね。
ではせっかくなのでやってみましょう。
アプローチ
SQLを実行する方法を探す
まずはMigrationクラス内でSQLを実行する方法を探します。
SQLの実行に必要なのは接続文字列。DbContextでは
…省略…
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=sample.db");
}
こんな感じで指定しています。これを取得して使う形になると思いますがはてさて…
MigrationBuilder Class (Microsoft.EntityFrameworkCore.Migrations) | Microsoft Docs
Methods
Sql(String, Boolean) Builds an SqlOperation to execute raw SQL.
これやんけ。
SQLを直接実行できるメソッドがありました。さすがEF。
MigrationBuilder.Sql()を使用すればSQLを実行できるようです。
新しいテーブル定義で別名のテーブルを作る
新しいテーブルの作成については、既存テーブルの作成クエリを基に、変更部分に手を加える方法が一番楽かと思います。
私が使っているツールは「DB Browser For SQLite」です。
このツールですでに作成されているデータベースファイルを開くと

こんな感じでテーブルのCREATE用スクリプトを拾うこともできます。
今のCREATE用スクリプトは
CREATE TABLE "Products" (
"ProductId" TEXT NOT NULL CONSTRAINT "PK_Products" PRIMARY KEY,
"UnitPrice" INTEGER NOT NULL,
"Name" TEXT NULL
)
こんな感じ。なので別名テーブルは
CREATE TABLE "Products_temp" ( --テーブル名に「_temp」を付けた
"ProductId" TEXT NOT NULL CONSTRAINT "PK_Products" PRIMARY KEY,
"UnitPrice" NUMERIC NOT NULL, --ここをINT → NUMERICに変更
"Name" TEXT NULL
)
これでいいでしょう。プログラム中に記述する際はSQLのコメントは消しておきましょう。
新しいテーブルにデータをコピー
これは特に語ることはありません。普通にinsertを実行します。
insert into Products_temp(
ProductId, UnitPrice, Name
)
select ProductId, UnitPrice, Name
from Products
まぁこんな感じでしょうか。
古いテーブルを削除
これも普通にdropを使いましょう。
drop table Products
新テーブルの名前を変更する
最後です。テーブル名を古いテーブルと同じにします。
alter table Products_temp rename to Products
Migration.Upメソッドに実装する
ここまでのSQL文を、MigrationクラスのUpメソッドで実行されるように実装していきます。
なお、今回は関係ありませんが、[SQLite Query Language: ALTER TABLE]によると、スキーマ変更前には外部キー制約をオフにしなさいよ的な記述があるので、併せて実装します。
protected override void Up(MigrationBuilder migrationBuilder)
{
/*//Original Code
migrationBuilder.AlterColumn<decimal>(
name: "UnitPrice",
table: "Products",
type: "numeric",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
*/
//ここから追加
//外部キーを無効にする
migrationBuilder.Sql("PRAGMA foreign_keys=false;");
string sql;
//新しいテーブル定義で別名のテーブルを作る
sql = @"CREATE TABLE ""Products_temp"" ( ";
sql += @" ""ProductId"" TEXT NOT NULL CONSTRAINT ""PK_Products"" PRIMARY KEY, ";
sql += @" ""UnitPrice"" NUMERIC NOT NULL, ";
sql += @" ""Name"" TEXT NULL";
sql += @")";
migrationBuilder.Sql(sql);
//新しいテーブルにデータをコピー
sql = "insert into Products_temp(";
sql += " ProductId, UnitPrice, Name ";
sql += ") ";
sql += "select ProductId, UnitPrice, Name ";
sql += " from Products";
migrationBuilder.Sql(sql);
//古いテーブルを削除
sql = "drop table Products; ";
migrationBuilder.Sql(sql);
//新テーブルの名前を変更する
sql = "alter table Products_temp rename to Products";
migrationBuilder.Sql(sql);
//外部キーを有効にする
migrationBuilder.Sql("PRAGMA foreign_keys=true;");
}
逆の内容をDownメソッドに実装する
Migrationクラスには、変更を元に戻すためのDownメソッドも存在します。元に戻す変更なので、今回の変更と逆の変更を実行するSQLを実装します。
アプローチは割愛して結果だけ掲載します。
protected override void Down(MigrationBuilder migrationBuilder)
{
/*//Original Code
migrationBuilder.AlterColumn<int>(
name: "UnitPrice",
table: "Products",
type: "INTEGER",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric");
*/
//ここから追加
//外部キーを無効にする
migrationBuilder.Sql("PRAGMA foreign_keys=false;");
string sql;
//新しいテーブル定義で別名のテーブルを作る
sql = @"CREATE TABLE ""Products_temp"" ( ";
sql += @" ""ProductId"" TEXT NOT NULL CONSTRAINT ""PK_Products"" PRIMARY KEY, ";
sql += @" ""UnitPrice"" INT NOT NULL, "; //元テーブルの定義なのでINT型。
sql += @" ""Name"" TEXT NULL";
sql += @")";
migrationBuilder.Sql(sql);
//新しいテーブルにデータをコピー
sql = "insert into Products_temp(";
sql += " ProductId, UnitPrice, Name ";
sql += ") ";
sql += "select ProductId, UnitPrice, Name ";
sql += " from Products";
migrationBuilder.Sql(sql); //NUMERICからINTへの暗黙的な変換。場合によってはここでコケるかもしれません!
//古いテーブルを削除
sql = "drop table Products; ";
migrationBuilder.Sql(sql);
//新テーブルの名前を変更する
sql = "alter table Products_temp rename to Products";
migrationBuilder.Sql(sql);
//外部キーを有効にする
migrationBuilder.Sql("PRAGMA foreign_keys=true;");
}
いざupdate-database実行
ここまでやったのでもう一度DBの更新を実行します。ちゃんと通るかな…?
update-database
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = '__EFMigrationsHistory' AND "type" = 'table';
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = '__EFMigrationsHistory' AND "type" = 'table';
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "MigrationId", "ProductVersion"
FROM "__EFMigrationsHistory"
ORDER BY "MigrationId";
Applying migration '20191005092253_UnitPriceToDecimal'.
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
PRAGMA foreign_keys=false;
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Products_temp" ( "ProductId" TEXT NOT NULL CONSTRAINT "PK_Products" PRIMARY KEY, "UnitPrice" NUMERIC NOT NULL, "Name" TEXT NULL)
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
insert into Products_temp( ProductId, UnitPrice, Name ) select ProductId, UnitPrice, Name from Products
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
drop table Products;
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
alter table Products_temp rename to Products
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
PRAGMA foreign_keys=true;
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20191005092253_UnitPriceToDecimal', '3.0.0');
Done.
Done!!
出来たのでは?
テーブル定義を確認します。

「"UnitPrice" NUMERIC NOT NULL」ということで無事NUMERIC型になっています。
プログラムを実行してみます(前回とデータが違う点はご愛嬌ということでよろしくです)。



一番下の行が新しく追加したレコードです。小数を保存できています。

データベースのデータも問題ありません。
感想その他雑記
- SQLの組み方とか、このSQLはまずいでしょ…みたいのがあったら教えてください。
- いろんな変更が簡単に実行できるようになるといいですね。次バージョンに期待しています。
- 今回は1テーブルの1列、それも特に外部キーとか関係ない列の定義変えるだけでしたが、結構な手順でした。
- インデックスとかの要素が絡んでくる変更で、しかも複数列複数テーブルとかになると…考えたくもないな!
以上です。