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();
}
}
}

浙公網安備 33010602011771號