Viewのマイグレーションを考える
異なるスキーマを参照するViewを作って、Entity Frameworkから参照したいという要件があって、Viewの更新管理をどうにかしたいなーと思っていたんだけれど、だいたい固まったのでメモしておく
方針としてはこんな感じ
- Viewの形のEntityを作り、DbContextにDbQueryとして公開する
- OnModelCreatingで、Query~ToViewで関連付ける
- 空のマイグレーションファイルを作り、upでcreate viewを、downでdrop viewを行う。スキーマの差し替えもこのタイミングで行う。
サンプル
https://github.com/karuakun/EntityFrameworkSchemaAcrossDbViewSample
EntityとDbContext
1,2はササッとこんな感じで
Data/Entities/QueryTest.cs
public class Test1 { [Key] public string Id { get; set; } public string Name { get; set; } public string QueryTest2Id { get; set; } }
Data/SampleDbContext.cs
public class SampleDbContext: DbContext { public const string DefaultConnectionStringName = "sampledb"; public SampleDbContext(DbContextOptions options) : base(options) { } public virtual DbQuery QueryTest2 { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Query(entity => { entity.ToView("v_seconddb_test2"); }); } }
マイグレーションの作成
DbContextにマイグレーションに関わる変更がない状態で、マイグレーションファイルを追加すると、空のマイグレーションファイルが出来上がるので、この子にViewの作成と削除を書いてあげる。
dotnet migrations add addsampleview
追加されたマイグレーションファイル
public partial class addview : Migration { protected override void Up(MigrationBuilder migrationBuilder) { } protected override void Down(MigrationBuilder migrationBuilder) { } }
まずは単純に
create view にスキーマ名が入っているので気持ち悪い。。。
public partial class addview : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.Sql( "create view v_seconddb_test2 as select Id, Name, Property1 from schema2.test2"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.Sql("drop view v_seconddb_test2"); } }
Configurationもしくは環境変数から、マッピングを取得する
Startup時に作るIConfigurationRootにアクセスできれば良いんだけれど、MigrationだとDIでConfigurationを受け取ることができない。
ConfigurationをStartupのstaticにしてあげるのは嫌なので、、、DesignTimeContextを使う
こんなクラスをプロジェクトのルートに置いておくと、dotnet efで起動した場合にこっちのコンテキストで初期化してくれる。これなら多少無茶してもいいかなぁ、、、と言うことで、IConfigurationRootをstaticで生やしてあげる。
public class DesignTimeSampleDbContextFactory : IDesignTimeDbContextFactory { public static IConfigurationRoot DesignTimeConfiguration { get; private set; } public SampleDbContext CreateDbContext(string[] args) { DesignTimeConfiguration = DesignTimeConfigurationBuilder.BuildConfiguration(args); var builder = new DbContextOptionsBuilder() .UseMySql(DesignTimeConfiguration.GetConnectionString(SampleDbContext.DefaultConnectionStringName) ); return new SampleDbContext(builder.Options); } }
Configurationを参照できれば、あとはJSONと環境変数の世界なので、appsettings.jsonにこんな感じでエントリを追加して
"MigrationSettings": { "MigrationType": "Local", "SchemaMappings": { "Local": { "AppDb": "appdb", "SecondDb": "seconddb" }, "Stating": { "AppDb": "stg_appdb", "SecondDb": "stg_seconddb" } } },
設定を受けるようにこんなクラスを作っておく
public class MigrationSchemaMappings { public string AppDb { get; set; } public string SecondDb { get; set; } }
で、手作業で作ったSQLがマイグレーションの海に埋もれるのは悲しいので、手作業で作った子はマイグレーション配下にこんな感じでmanualフォルダーを作って、そこに物理ファイルとしておいておきたい。
マイグレーションの拡張メソッドとして、こんな感じのものを作っておけば、
public static class MigrationExtensions { public static string GetSqlText(this Migration source, string migrationId, string fileName) { var sqlPath = Path.Combine(Environment.CurrentDirectory, "Migrations", "manual", migrationId, fileName); if (!File.Exists(sqlPath)) throw new FileNotFoundException(sqlPath); var sqlText = File.ReadAllText(sqlPath); if (string.IsNullOrEmpty(sqlText)) throw new InvalidOperationException($"sqlfile is empty: {sqlText}"); var settings = GetMigrationSettings(); var schemaReplacedSqlText = sqlText.Replace("$SecondDb$", settings.SecondDb); return schemaReplacedSqlText; } private static MigrationSchemaMappings GetMigrationSettings() { var configuration = DesignTimeSampleDbContextFactory.DesignTimeConfiguration; var migrationType = configuration.GetValue("MigrationSettings:MigrationType"); return configuration.GetSection($"MigrationSettings:SchemaMappings:{migrationType}") .Get(); } }
マイグレーションスクリプトはこうかける。
public partial class addview : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.Sql(this.GetSqlText(this.GetId(), "up.v_seconddb_test2.sql")); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.Sql(this.GetSqlText(this.GetId(), "down.v_seconddb_test2.sql")); } }