ライブラリ Entity Framework Core (以下、EFCore) を使って、SQL Server などのリレーショナルデータベースに読み書きする、C# プログラミングにおける話。
とりわけ、"Code First" と呼ばれる、C# コード側でデータベースの構造 (テーブルや列) を記述し、その記述に従ってデータベース側を自動で構築・更新 (マイグレーション) する、開発運用スタイルにおける話題である。
EFCore におけるマイグレーションについて詳しくは、公式ドキュメントを参照されたし。
さてさて、通常、EFCore におけるデータベースのマイグレーションは、以下のようなコードで実行される。
await dbContext.Database.MigrateAsync();
上記コードが実行されると、このデータベースコンテキストの接続先データベースにあるマイグレーションの履歴テーブルに記録されてる履歴と、実行中のプログラムに定義されているマイグレーションの定義とを照合する。
そして、未適用のマイグレーションが実行中のプログラムに定義されていれば、それら未適用のマイグレーション定義を適用する。
こうして、実行中のプログラムで定義されているデータベース構造と、実際のデータベース構造とが整合されて動作するようになる。
ちょっと奇妙な要件発生
さてある日、かなり奇妙ではあるが、未適用のマイグレーション定義をすべて適用するのではなく、指定のマイグレーション定義までに留めて適用したいという要件が発生してしまった。
つまり、とある C# プログラム内に、"M1", "M2", "M3" という3段階のマイグレーション定義が含まれているが、適用するのは "M2" までに留めたい、という要件だ。
このプログラムを実行したときに、接続先データベースに適用済みのマイグレーションと、期待される動作との対応は以下のとおりである。
接続先データベースに適用済みのマイグレーションが "M1" ⇒ "M2" を適用接続先データベースに適用済みのマイグレーションが "M2" ⇒ なにもしない接続先データベースに適用済みのマイグレーションが "M3" ⇒ なにもしない
あいにくと先に記した "MigrateAsync()" メソッドを実行すると、そのプログラムに含まれるすべての未適用マイグレーション定義が適用されてしまう (上記例でいうと、いずれのシナリオでも "M3" まで適用されてしまう) ので、この要件を満たせず困ってしまった、という話だ。
何ともややこしい話で、なぜそのような需要が発生するのか見当もつかない読者も多いと思うし、もしかするともっと別の作戦・戦略で解決すべき課題だったのかもしれない。
しかし書き始めると長くなるので、ここは詳細割愛し、そういう要件が発生してしまった、という前提のまま話を進める。
EFCore に、ちゃんと用意があった
さてこのような要件を達成することができるのか、というと、いちおう、ちゃんと (?) EFCore 側に用意がある。
まず知っておくべき事として、実は、dbContext.Database オブジェクトは、IInfrastructure<IServiceProvider> インターフェースを実装している。
このインターフェース経由で dbContext.Database オブジェクトに問い合わせすると、dbContext.Database オブジェクトが隠し持っている (?) 各種サービスの参照を手に入れることができる。
そして、それらサービスのひとつとして、EFCore におけるマイグレーションの諸々を司る IMigrator インターフェースのサービスがある。
この IMigrator インターフェースには、指定の名前のマイグレーション定義にまでマイグレーションを進める、引数に対象マイグレーション定義名を持つ、"MigrateAsync(string)" (又はその同期バージョンである "Migrate(string)") メソッドが用意されているのだ。
具体的なコード例を書くと以下のような感じになる。
var services = dbContext.Database as IInfrastructure<IServiceProvider>;
var migrator = services.GetService<IMigrator>();
await migrator.MigrateAsync("M2");
これで指定したマイグレーション定義、上記例だと "M2" までのマイグレーション適用が可能となる。
もう一捻り
だがしかし、まだ上記コードでは足りない点がある。
というのも、IMigrator.MigrateAsync(string) メソッドは、指定されたマイグレーション定義まで、マイグレーション適用を "進める" だけでなく "戻す" 作用もあるからだ。
つまり、上記コード例だと、以下のように動作してしまう。
適用済みのマイグレーションが "M1" ⇒ "M2" を適用適用済みのマイグレーションが "M2" ⇒ なにもしない適用済みのマイグレーションが "M3" ⇒ "M3" をロールバック ("M2"適用相当にまで戻る)
言い換えると、IMigrator.MigrateAsync(string) メソッドは、指定されたマイグレーション定義までを適用したのと同じレベルに、接続先データベースの構造を整合させようとするわけだ。
もちろん、この動作が望ましいシナリオも多々あると思う。
しかし今回の要件は、すでに "M2" までが適用すみであれば、接続先データベースが実際には "M3" まで進んでいたとしても、何もしないことが要件だ。
そこで今回要件に対応するためには、もう一捻りが必要、というわけである。
幸い、これは簡単である。
接続先データベースに適用済みのマイグレーション定義名は、GetAppliedMigrations() というメソッドでマイグレーション定義名文字列の集合として取得できる。
これと照合して、希望のマイグレーションが適用済みでない場合に限って指定マイグレーションまでを適用、とすればよい次第。
具体的なコード例を以下に示す。
var appliedMigratiosn = dbContext.Database.GetAppliedMigrations();
// "M2" マイグレーションが、適用済みマイグレーション一覧にない場合...
if (!appliedMigratiosn.Contains("M2")){
// 以下は先に紹介のコードに同じ
var services = dbContext.Database as IInfrastructure<IServiceProvider>;
var migrator = services.GetService<IMigrator>();
await migrator.MigrateAsync("M2");
}
以上のコードで、今回要件に対応完了とすることができた。
おわりに
今回要件の話は極めて特殊であろう。
しかしながら、
DbContext.Database オブジェクトが IInfrastructure<IServiceProvider> インターフェースを実装している事であるとか、そのサービスプロバイダから EFCore 管理下のサービスオブジェクトを入手できること、IMigrator インターフェースをもつサービスオブジェクトの存在
などなど、今回のエピソードを通して、何かしらピンチの時にヒントになりそうな知見を手に入れられたと思う。
とりわけ、"Code First" と呼ばれる、C# コード側でデータベースの構造 (テーブルや列) を記述し、その記述に従ってデータベース側を自動で構築・更新 (マイグレーション) する、開発運用スタイルにおける話題である。
EFCore におけるマイグレーションについて詳しくは、公式ドキュメントを参照されたし。
さてさて、通常、EFCore におけるデータベースのマイグレーションは、以下のようなコードで実行される。
await dbContext.Database.MigrateAsync();
上記コードが実行されると、このデータベースコンテキストの接続先データベースにあるマイグレーションの履歴テーブルに記録されてる履歴と、実行中のプログラムに定義されているマイグレーションの定義とを照合する。
そして、未適用のマイグレーションが実行中のプログラムに定義されていれば、それら未適用のマイグレーション定義を適用する。
こうして、実行中のプログラムで定義されているデータベース構造と、実際のデータベース構造とが整合されて動作するようになる。
ちょっと奇妙な要件発生
さてある日、かなり奇妙ではあるが、未適用のマイグレーション定義をすべて適用するのではなく、指定のマイグレーション定義までに留めて適用したいという要件が発生してしまった。
つまり、とある C# プログラム内に、"M1", "M2", "M3" という3段階のマイグレーション定義が含まれているが、適用するのは "M2" までに留めたい、という要件だ。
このプログラムを実行したときに、接続先データベースに適用済みのマイグレーションと、期待される動作との対応は以下のとおりである。
接続先データベースに適用済みのマイグレーションが "M1" ⇒ "M2" を適用接続先データベースに適用済みのマイグレーションが "M2" ⇒ なにもしない接続先データベースに適用済みのマイグレーションが "M3" ⇒ なにもしない
あいにくと先に記した "MigrateAsync()" メソッドを実行すると、そのプログラムに含まれるすべての未適用マイグレーション定義が適用されてしまう (上記例でいうと、いずれのシナリオでも "M3" まで適用されてしまう) ので、この要件を満たせず困ってしまった、という話だ。
何ともややこしい話で、なぜそのような需要が発生するのか見当もつかない読者も多いと思うし、もしかするともっと別の作戦・戦略で解決すべき課題だったのかもしれない。
しかし書き始めると長くなるので、ここは詳細割愛し、そういう要件が発生してしまった、という前提のまま話を進める。
EFCore に、ちゃんと用意があった
さてこのような要件を達成することができるのか、というと、いちおう、ちゃんと (?) EFCore 側に用意がある。
まず知っておくべき事として、実は、dbContext.Database オブジェクトは、IInfrastructure<IServiceProvider> インターフェースを実装している。
このインターフェース経由で dbContext.Database オブジェクトに問い合わせすると、dbContext.Database オブジェクトが隠し持っている (?) 各種サービスの参照を手に入れることができる。
そして、それらサービスのひとつとして、EFCore におけるマイグレーションの諸々を司る IMigrator インターフェースのサービスがある。
この IMigrator インターフェースには、指定の名前のマイグレーション定義にまでマイグレーションを進める、引数に対象マイグレーション定義名を持つ、"MigrateAsync(string)" (又はその同期バージョンである "Migrate(string)") メソッドが用意されているのだ。
具体的なコード例を書くと以下のような感じになる。
var services = dbContext.Database as IInfrastructure<IServiceProvider>;
var migrator = services.GetService<IMigrator>();
await migrator.MigrateAsync("M2");
これで指定したマイグレーション定義、上記例だと "M2" までのマイグレーション適用が可能となる。
もう一捻り
だがしかし、まだ上記コードでは足りない点がある。
というのも、IMigrator.MigrateAsync(string) メソッドは、指定されたマイグレーション定義まで、マイグレーション適用を "進める" だけでなく "戻す" 作用もあるからだ。
つまり、上記コード例だと、以下のように動作してしまう。
適用済みのマイグレーションが "M1" ⇒ "M2" を適用適用済みのマイグレーションが "M2" ⇒ なにもしない適用済みのマイグレーションが "M3" ⇒ "M3" をロールバック ("M2"適用相当にまで戻る)
言い換えると、IMigrator.MigrateAsync(string) メソッドは、指定されたマイグレーション定義までを適用したのと同じレベルに、接続先データベースの構造を整合させようとするわけだ。
もちろん、この動作が望ましいシナリオも多々あると思う。
しかし今回の要件は、すでに "M2" までが適用すみであれば、接続先データベースが実際には "M3" まで進んでいたとしても、何もしないことが要件だ。
そこで今回要件に対応するためには、もう一捻りが必要、というわけである。
幸い、これは簡単である。
接続先データベースに適用済みのマイグレーション定義名は、GetAppliedMigrations() というメソッドでマイグレーション定義名文字列の集合として取得できる。
これと照合して、希望のマイグレーションが適用済みでない場合に限って指定マイグレーションまでを適用、とすればよい次第。
具体的なコード例を以下に示す。
var appliedMigratiosn = dbContext.Database.GetAppliedMigrations();
// "M2" マイグレーションが、適用済みマイグレーション一覧にない場合...
if (!appliedMigratiosn.Contains("M2")){
// 以下は先に紹介のコードに同じ
var services = dbContext.Database as IInfrastructure<IServiceProvider>;
var migrator = services.GetService<IMigrator>();
await migrator.MigrateAsync("M2");
}
以上のコードで、今回要件に対応完了とすることができた。
おわりに
今回要件の話は極めて特殊であろう。
しかしながら、
DbContext.Database オブジェクトが IInfrastructure<IServiceProvider> インターフェースを実装している事であるとか、そのサービスプロバイダから EFCore 管理下のサービスオブジェクトを入手できること、IMigrator インターフェースをもつサービスオブジェクトの存在
などなど、今回のエピソードを通して、何かしらピンチの時にヒントになりそうな知見を手に入れられたと思う。