如何有效控制并管理Ef Core的数据库迁移过程?

摘要:Ef Core花里胡哨系列(8) 如何可控管理Ef Core的迁移? 通常使用Ef Core迁移时,可能就是简单的使用命令dotnet-ef migrations add或者dotnet ef database update等等,基本都需要
Ef Core花里胡哨系列(8) 如何可控管理Ef Core的迁移? 通常使用Ef Core迁移时,可能就是简单的使用命令dotnet-ef migrations add或者dotnet ef database update等等,基本都需要靠命令维护,非常的繁琐。特别是现在很多项目都是迭代型项目,很容易造成开发人员和运维人员的负担,所以,我们是否可以将其自动化? 自动迁移 自动迁移顾名思义,就是可以让程序启动的时候自己执行迁移,不需要运维人员参与,开发人员只需要保证迁移顺序的正确性即可。 自动建库 如果想使用首次自动建库,那我们就需要生成首次迁移时,直接删除首次迁移的文件,留下[Sample]DbContextModelSnapshot快照文件即可,当然,这些不是主要内容,只是用来引出下面的可控迁移。 var app = builder.Build(); await using var scoped = app.Services.CreateAsyncScope(); using var db = scoped.ServiceProvider.GetRequiredService<SampleDbContext>(); try { // db.Database.Migrate(); // 需要保留首次迁移文件,并且后续启动可以自动迁移 db.Database.EnsureCreated(); // 不需要保留首次迁移文件 } catch { Console.WriteLine("init database error."); } 可控迁移 可控迁移即我们可以通过封装Ef Core内置的各种Service来帮助我们实现控制迁移的效果。 EfMigrationHistory 我们要可控迁移,那么我们就需要想办法操控__EFMigrationsHistory这张表,它是Ef Core内置的表,用来记录迁移的记录,这张表是一张无状态的表,他只负责存储成功的迁移名称和迁移时Ef Core的版本,其它没有关联,我们如何管理它呢?我们只需在DbContext中创建一个同名的表即可,并且可以预先设计好其它审计用字段,后续不可更改。 例如我们重新设计这张表,除了迁移IdMigrationId和Ef Core的版本ProductVersion外,我们添加迁移应用时间,和迁移应用类型字段。 [Table("__EFMigrationsHistory")] public class EFMigrationsHistory { [Key] [MaxLength(150)] public required string MigrationId { get; set; } [MaxLength(32)] public string ProductVersion { get; set; } = null!; [NotMapped] public string Sort => MigrationId.Split("_")[0]; [Comment("迁移时间")] public DateTime? MigrationTime { get; set; } = DateTime.Now; [Column(TypeName = "varchar(20)")] public MigrationType MigrationType { get; set; } = MigrationType.Success; } 添加迁移类型的目的是为了增加迁移执行顺序的丰富性,下面提供了成功Success、尝试但失败TryFail、尝试但成功TrySuccess以及跳过Skip等多种方式。其中History和Install为记录型,主要是为了标记历史记录和安装时间。 public enum MigrationType { Success, TryFail, TrySuccess, Skip, History, Install } 随后我们将其添加到DbContext上下文中即可,我们的新结构会替代原来的结构实行职能。 public interface IMigrationDbContext { DbSet<EFMigrationsHistory> EFMigrationsHistory { get; set; } } public class SampleDbContext : IMigrationDbContext { public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options) { } public DbSet<EFMigrationsHistory> EFMigrationsHistory { get; set; } } 迁移控制 我们已经将__EFMigrationsHistory注册到了DbContext的上下文中,成为了我们可用的表,我们接下来就是了解Ef Core是如何迁移的,我们如何加入自己的逻辑。 我们之前有提到__EFMigrationsHistory只记录了成功的迁移,如果迁移没有成功,则会立即中断,那么他是怎么实现的?其实就是读取本地的文件列表,然后按照迁移名称进行排序并和表中的对比,然后开始逐一执行。 如此我们就可以模仿他的操作,来实现我们自己的逻辑。 迁移管理 我喜欢将迁移输出到类库,这个方便读取和管理,需要调用的地方只需引用该类库即可。 services.AddDbContext<SampleDbContext>(opts => { optionsBuilder.UseMySql(connStr, new MySqlServerVersion(new Version(8, 0)), opts => opts.MigrationsAssembly("Sample.Migrations")); }); 每个迁移文件都分为两部分,xxxx.cs和xxxx.Designer.cs,其中xxxx.Designer.cs中以下部分是我们需要的部分。 [DbContext(typeof(SampleDbContext))]这个特性向我们指明了迁移对应的DbContext是哪个,也就是说,我们可以自定义多个DbContext从其判断执行不同的迁移。 [Migration("20231108075812_XXXXX")]这个特性向我们指明了迁移名称,当然,也代表了迁移的顺序。 [DbContext(typeof(SampleDbContext))] [Migration("20231108075812_XXXXX")] partial class 20231108075812_XXXXX { } 而且,我们可以根据需要扩充这些特性,来满足我们不同的顺序需求: /// <summary> /// 标记的迁移失败会自动跳过 /// </summary> [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class TryMigrationsAttribute : Attribute { public TryMigrationsAttribute() { } } /// <summary> /// 标记需要跳过的Migration /// </summary> [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class SkipMigrationsAttribute : Attribute { public SkipMigrationsAttribute() { } } 好了,所有的东西都准备完成了,我们来实现真正的迁移管理。 我们首选需要获取迁移所在的程序集: private static Assembly EfAssembly => Assembly.Load("Sample.Migrations"); 然后根据需要,获取DbContext中的迁移服务,就可以迁移指定的迁移了: var migrator = dbContext.GetService<IMigrator>(); 以下是完整的代码,包括多个DbContext如何实现互不干扰以及指定迁移,自动归档迁移等等。 public class MigrationManager<TMainDbContext, TMySqlDbContext, TOracleDbContext, TPostgreSQLDbContext, TSqlServerDbContext> where TMainDbContext : DbContext, IMigrationDbContext, new() where TMySqlDbContext : DbContext, IMigrationDbContext, new() where TOracleDbContext : DbContext, IMigrationDbContext, new() where TPostgreSQLDbContext : DbContext, IMigrationDbContext, new() where TSqlServerDbContext : DbContext, IMigrationDbContext, new() { private readonly IServiceProvider ServiceProvider; private readonly ILogger Logger; protected TMainDbContext DB => ServiceProvider.GetRequiredService<TMainDbContext>(); public MigrationManager(IServiceProvider serviceProvider, ILogger logger) { ServiceProvider = serviceProvider; Logger = logger; } private static Assembly EfAssembly => Assembly.Load("Sample.Migrations"); private static DatabaseType DbType => AppSettings.Get<DbOptions>()?.GetUseableWriteHost()?.DbType ?? DatabaseType.MySql; private static string DbTypeName => DbType.ToString(); public static string GetEfVersion() { return Microsoft.EntityFrameworkCore.Infrastructure.ProductInfo.GetVersion(); } /// <summary> /// 获取的是本地文件中的所有版本,而不是数据库 /// </summary> /// <returns></returns> public IEnumerable<EFMigrationsHistory> GetList() { var migrations = EfAssembly.GetTypes().Where(x => x.Namespace != null && x.Namespace.Contains(DbTypeName) && x.GetCustomAttributes<MigrationAttribute>().Any() && x.GetCustomAttribute<DbContextAttribute>()?.ContextType.BaseType == typeof(TMainDbContext)) .Select(x => new EFMigrationsHistory() { MigrationId = x.GetCustomAttribute<MigrationAttribute>()!.Id, ProductVersion = GetEfVersion(), }) .AsQueryable(); return migrations; } public IEnumerable<Type> GetClassList() { var migrations = EfAssembly.GetTypes().Where(x => x.Namespace != null && x.Namespace.Contains(DbTypeName) && x.GetCustomAttributes<MigrationAttribute>().Any() && x.GetCustomAttribute<DbContextAttribute>()?.ContextType.BaseType == typeof(TMainDbContext)) .AsQueryable(); return migrations; } /// <summary> /// 将部署前的迁移都加入迁移记录表中 /// </summary> /// <returns></returns> public async Task DiscardNoPendingMigrationsHistoryAsync() { Logger.LogInformation("Migration-Discard: 准备部署前写入历史迁移"); var lastVersion = await GetLastVersion(); Logger.LogInformation($"Migration-Discard: 部署前迁移最后版本 [{lastVersion?.MigrationId}]"); var applied = await GetAppliedMigrationsAsync(); Logger.LogInformation($"Migration-Discard: 部署前已经应用的迁移 [{applied.Count()}] 条记录"); var migrations = lastVersion is null ? GetList().ToList() : GetList().Where(x => String.CompareOrdinal(x.Sort, lastVersion.Sort) <= 0 && (applied != null && !applied.Contains(x.MigrationId))).ToList(); if (migrations is not null && migrations.Any()) { migrations.ForEach(x=>x.MigrationType = MigrationType.History); DB.AddRange(migrations); await DB.SaveChangesAsync(); } Logger.LogInformation($"Migration-Discard: 部署前写入的历史迁移 [{migrations?.Count ?? 0}] 条记录"); } /// <summary> /// 获取最后一个版本 /// </summary> /// <returns></returns> public async Task<EFMigrationsHistory?> GetLastVersion() { var applied = await DB.EFMigrationsHistory.ToListAsync(); var thisContextLocal = GetList().Select(x => x.MigrationId); var thisContextVersions = applied.Where(x => thisContextLocal.Contains(x.MigrationId)); return thisContextVersions.MaxBy(x => x.Sort); } /// <summary> /// 获取未应用的迁移 /// </summary> /// <returns></returns> public async Task<IEnumerable<EFMigrationsHistory>> GetPendingMigrationsAsync() { var lastVersion = await GetLastVersion(); if (lastVersion is null) { return GetList(); } return GetList().Where(x => String.CompareOrdinal(x.Sort, lastVersion.Sort) > 0).OrderBy(x => x.Sort); } public async Task<IEnumerable<Type>> GetPendingMigrationsClassAsync() { var lastVersion = await GetLastVersion(); if (lastVersion is null) { return GetClassList(); } return GetClassList().Where(x => String.CompareOrdinal(x.GetCustomAttribute<MigrationAttribute>()!.Id, lastVersion.MigrationId) > 0).OrderBy(x => x.GetCustomAttribute<MigrationAttribute>()!.Id); } private IMigrator GetMigrator() { DbContext dbContext = DbType switch { DatabaseType.SqlServer => new TSqlServerDbContext(), DatabaseType.Oracle => new TOracleDbContext(), DatabaseType.PostgreSQL => new TPostgreSQLDbContext(), DatabaseType.MySql or _ => new TMySqlDbContext(), }; return dbContext.GetService<IMigrator>(); } /// <summary> /// 应用迁移 /// </summary> /// <param name="migrations"></param> /// <returns></returns> public async Task ApplyMigrationsAsync(IEnumerable<EFMigrationsHistory> migrations) { if (!migrations.Any()) { return; } var migrator = GetMigrator(); foreach (var migration in migrations) { await migrator.MigrateAsync(migration.MigrationId); } } /// <summary> /// 应用迁移, 会跳过尝试迁移失败的迁移 /// </summary> /// <param name="migrations"></param> /// <returns></returns> public async Task ApplyMigrationsClassAsync(IEnumerable<Type> migrations) { if (!migrations.Any()) { return; } var migrator = GetMigrator(); var skipMigrations = new List<EFMigrationsHistory>(); foreach (var migration in migrations) { var id = migration.GetCustomAttribute<MigrationAttribute>()!.Id; if (migration.GetCustomAttributes<TryMigrationsAttribute>().Any()) { try { await migrator.MigrateAsync(id); } catch (Exception ex) { var skip = new EFMigrationsHistory { MigrationId = id, ProductVersion = GetEfVersion(), MigrationType = MigrationType.TryFail }; var errMsg = @$"迁移 [{id}] 已尝试并跳过!\n 迁移失败!\n 提示信息为: {ex.Message}\n 请检查 Migrations 文件! 或者手动更改 __MigrationsHistory 表, 将该迁移添加入表中标记为已应用即可. "; Logger.LogWarning(ex, errMsg); DB.AddRange(skip); await DB.SaveChangesAsync(); } } else if (migration.GetCustomAttributes<SkipMigrationsAttribute>().Any()) { var skip = new EFMigrationsHistory { MigrationId = id, ProductVersion = GetEfVersion(), MigrationType = MigrationType.Skip }; var errMsg = @$"迁移 [{id}] 已跳过!\n 或者手动更改 __MigrationsHistory 表, 将该迁移添加入表中标记为已应用即可. "; Logger.LogWarning(errMsg); DB.AddRange(skip); await DB.SaveChangesAsync(); } else { try { await migrator.MigrateAsync(id); } catch (Exception ex) { var errMsg = @$"迁移 [{id}] 失败!\n 迁移失败!\n 提示信息为: {ex.Message}\n 请检查 Migrations 文件! 或者手动更改 __MigrationsHistory 表, 将该迁移添加入表中标记为已应用即可. "; Logger.LogWarning(ex, errMsg); } } Logger.LogInformation($"迁移 [{id}] 已应用."); } } /// <summary> /// 获取已应用的迁移 /// </summary> /// <returns></returns> public async Task<IEnumerable<string>> GetAppliedMigrationsAsync() { return await DB.Database.GetAppliedMigrationsAsync(); } /// <summary> /// 开始迁移入口 /// </summary> /// <returns></returns> public async Task StartupInitMigrationsAsync() { Logger.LogInformation($"Migration-Main: 迁移程序开始执行..."); var migrationsHistory = await GetAppliedMigrationsAsync(); Logger.LogInformation($"Migration-Main: 已应用的迁移 [{string.Join(",", migrationsHistory)}]"); if (migrationsHistory.Any()) { var pendingMigrations = await GetPendingMigrationsClassAsync(); Logger.LogInformation($"Migration-Main: 获取未应用的迁移 [{string.Join(",", pendingMigrations.Select(x => x.GetCustomAttribute<MigrationAttribute>()!.Id))}]"); await ApplyMigrationsClassAsync(pendingMigrations); } await DiscardNoPendingMigrationsHistoryAsync(); } public async Task EnsureHasEfMigrationsHistoryTableAsync() { if (!await DB.IsExistTableAsync(typeof(EFMigrationsHistory))) { await DB.EnsureTableCreatedAsync(typeof(EFMigrationsHistory)); var migrationInit = new EFMigrationsHistory() { MigrationId = $"{DateTime.UtcNow.AddMonths(-6).ToString("yyyyMMddHHmmss")}_Install", MigrationType = MigrationType.Install, ProductVersion = MigrationManager<DataDbContext, MySqlDbContext, OracleDbContext, PostgreSQLDbContext, SqlServerDbContext>.GetEfVersion() }; DB.Add(migrationInit); await DB.SaveChangesAsync(); } } }