Ef Core花里胡哨系列(8) 如何可控管理Ef Core的迁移?
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();
}
}
}