数据库迁移
数据库上下文
数据库迁移时上下文的来源:
- 指定启动项目,在启动项目的服务容器里获取数据库上下文
- 无法在服务容器 DI 里获取,者尝试通过无参构造器生成数据库上下文
-
如果存在
IDesignTimeDbContextFactory<TContext>
,优先级最高还可以通过实现 Microsoft.EntityFrameworkCore.Design.IDesignTimeDbContextFactory接口来告知工 具如何创建 DbContext:如果在与派生的 DbContext 相同的项目中或在应用程序的启动项目中找到 实现此接口的类,则这些工具会绕过创建 DbContext 的其他方式,转而使用设计时工厂
ABP 默认使用了 IDesignTimeDbContextFactory 生成迁移上下文 dotnet ef migrations add int1
dotnet ef database update
需要注意的是在 rider 里需要在生成迁移文件后,运行下编译项目
public class abpDbContextFactory : IDesignTimeDbContextFactory<abpDbContext>
{
public abpDbContext CreateDbContext(string[] args)
{
// https://www.npgsql.org/efcore/release-notes/
// 6.0.html#opting-out-of-the-new-timestamp-mapping-logic
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
//更改模型的映射关系,在数据库上下文里也可以更改
abpEfCoreEntityExtensionMappings.Configure();
var configuration = BuildConfiguration();
//设置上下文提供程序
var builder = new DbContextOptionsBuilder<abpDbContext>()
.UseNpgsql(configuration.GetConnectionString("Default"));
//生成上下文
return new abpDbContext(builder.Options);
}
private static IConfigurationRoot BuildConfiguration()
{
var builder = new ConfigurationBuilder()
//设置文件提供程序的来源,因为默认是在当前项目
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory(),
"../wind.abp.DbMigrator/"))
.AddJsonFile("appsettings.json", optional: false);
return builder.Build();
}
}
迁移类 wind.abp.DbMigrator
abp.vnext 默认会生成一个迁移类,迁移类作为一个 host 服务
public class DbMigratorHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IConfiguration _configuration;
public DbMigratorHostedService(
IHostApplicationLifetime hostApplicationLifetime, IConfiguration configuration)
{
_hostApplicationLifetime = hostApplicationLifetime;
_configuration = configuration;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
//直接生成 abp AbpApplicationFactory 为了后面的注入服务,
//这里没有提供外部的 IServiceCollection 表达的意思是生成内部模块
using (var application = await AbpApplicationFactory.
CreateAsync<abpDbMigratorModule>(options =>
{
options.Services.ReplaceConfiguration(_configuration);
options.UseAutofac();
options.Services.AddLogging(c => c.AddSerilog());
options.AddDataMigrationEnvironment();
}))
{
//初始化模块,只要模块自己的服务
await application.InitializeAsync();
await application
.ServiceProvider
//迁移服务
.GetRequiredService<abpDbMigrationService>()
.MigrateAsync();
await application.ShutdownAsync();
_hostApplicationLifetime.StopApplication();
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpCachingStackExchangeRedisModule),
typeof(abpEntityFrameworkCoreModule),
typeof(abpApplicationContractsModule)
)]
public class abpDbMigratorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDistributedCacheOptions>(options => { options.KeyPrefix = "abp:"; });
}
}
迁移服务
public class abpDbMigrationService : ITransientDependency
{
public ILogger<abpDbMigrationService> Logger { get; set; }
private readonly IDataSeeder _dataSeeder;
private readonly IEnumerable<IabpDbSchemaMigrator> _dbSchemaMigrators;
private readonly ITenantRepository _tenantRepository;
private readonly ICurrentTenant _currentTenant;
public abpDbMigrationService(
IDataSeeder dataSeeder,
//迁移数据库
IEnumerable<IabpDbSchemaMigrator> dbSchemaMigrators,
ITenantRepository tenantRepository,
ICurrentTenant currentTenant)
{
_dataSeeder = dataSeeder;
_dbSchemaMigrators = dbSchemaMigrators;
_tenantRepository = tenantRepository;
_currentTenant = currentTenant;
Logger = NullLogger<abpDbMigrationService>.Instance;
}
public async Task MigrateAsync()
{
//判断迁移目录是否生成,这个方法只是第一次生成迁移目录
var initialMigrationAdded = AddInitialMigrationIfNotExist();
//如果迁移目录已经生产直接退出,要想执行迁移文件,需要在执行一次项目
//因为迁移文件生成需要时间,这里直接去执行迁移命令有可能文件还没有生成,
//故第一次生成文件的时候直接退出
//执行完成后生成迁移文件使用的 是 dotnet ef migrations add int1 命令,
//此命令生成后必须要编译前迁移文件所在的项目
//后面生成新的迁移文件必须要通过命令,或删除旧的迁移文件再次执行迁移项目,
//最好的方式还是通过命令比较方便
if (initialMigrationAdded)
{
//第一次生成迁移文件执行退出
return;
}
Logger.LogInformation("Started database migrations...");
//迁移到数据库
await MigrateDatabaseSchemaAsync();
//生成种子数据
await SeedDataAsync();
Logger.LogInformation($"Successfully completed host database migrations.");
//租户的迁移
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var migratedDatabaseSchemas = new HashSet<string>();
foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id))
{
if (tenant.ConnectionStrings.Any())
{
var tenantConnectionStrings = tenant.ConnectionStrings
.Select(x => x.Value)
.ToList();
if (!migratedDatabaseSchemas.IsSupersetOf(tenantConnectionStrings))
{
await MigrateDatabaseSchemaAsync(tenant);
migratedDatabaseSchemas.AddIfNotContains(tenantConnectionStrings);
}
}
await SeedDataAsync(tenant);
}
Logger.LogInformation(
$"Successfully completed {tenant.Name} tenant database migrations.");
}
Logger.LogInformation("Successfully completed all database migrations.");
Logger.LogInformation("You can safely end this process...");
}
//迁移到数据库
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation(
$"Migrating schema for
{(tenant == null ? "host" : tenant.Name + " tenant")} database...");
//迁移服务,在微服务里可以一次执行多个迁移
foreach (var migrator in _dbSchemaMigrators)
{
await migrator.MigrateAsync();
}
}
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" :
tenant.Name + " tenant")} database seed...");
//执行所有模块定义的种子数据生成,需要提供一个上下文,上下文供所有的种子数据贡献器使用
await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName,
IdentityDataSeedContributor.AdminEmailDefaultValue)
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName,
IdentityDataSeedContributor.AdminPasswordDefaultValue)
);
}
private bool AddInitialMigrationIfNotExist()
{
try
{
//迁移项目不存在在,直接退出
if (!DbMigrationsProjectExists())
{
return false;
}
}
catch (Exception)
{
return false;
}
try
{
//迁移目录已经存在直接退出
if (!MigrationsFolderExists())
{
AddInitialMigration();
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
Logger.LogWarning("Couldn't determinate if any migrations exist : " + e.Message);
return false;
}
}
private bool DbMigrationsProjectExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return dbMigrationsProjectFolder != null;
}
//迁移目录
private bool MigrationsFolderExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return dbMigrationsProjectFolder != null && Directory.Exists(
Path.Combine(dbMigrationsProjectFolder, "Migrations"));
}
//执行迁移目录 dotnent ef migrations add in1,这个命令要传入命令执行的目录,也就是数据库上下文所在的目录
private void AddInitialMigration()
{
Logger.LogInformation("Creating initial migration...");
string argumentPrefix;
string fileName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
argumentPrefix = "-c";
fileName = "/bin/bash";
}
else
{
argumentPrefix = "/C";
fileName = "cmd.exe";
}
var procStartInfo = new ProcessStartInfo(fileName,
$"{argumentPrefix} \"abp create-migration-and-run-migrator
\"{GetEntityFrameworkCoreProjectFolderPath()}\"\""
);
try
{
Process.Start(procStartInfo);
}
catch (Exception)
{
throw new Exception("Couldn't run ABP CLI...");
}
}
//找到 .EntityFrameworkCore 结尾的目录
private string? GetEntityFrameworkCoreProjectFolderPath()
{
var slnDirectoryPath = GetSolutionDirectoryPath();
if (slnDirectoryPath == null)
{
throw new Exception("Solution folder not found!");
}
var srcDirectoryPath = Path.Combine(slnDirectoryPath, "src");
return Directory.GetDirectories(srcDirectoryPath)
.FirstOrDefault(d => d.EndsWith(".EntityFrameworkCore"));
}
//找到 src 目录
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (currentDirectory != null && Directory.
GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (currentDirectory != null && Directory.
GetFiles(currentDirectory.FullName).
FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}
}
return null;
}
}
迁移数据接口 IabpDbSchemaMigrator
namespace wind.abp.EntityFrameworkCore;
public class EntityFrameworkCoreabpDbSchemaMigrator
: IabpDbSchemaMigrator, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
public EntityFrameworkCoreabpDbSchemaMigrator(
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task MigrateAsync()
{
/* We intentionally resolve the abpDbContext
* from IServiceProvider (instead of directly injecting it)
* to properly get the connection string of the current tenant in the
* current scope.
*/
//这里的数据库上下文是通过 DI 注入的,直接通过 database 提供器的迁移命令
await _serviceProvider
.GetRequiredService<abpDbContext>()
.Database
.MigrateAsync();
}
}
迁移种子数据接口
在任意模块定义此接口的实现类
public interface IDataSeedContributor
{
Task SeedAsync(DataSeedContext context);
}
abp DataSeeder 服务会执行加载模块的所有 IDataSeedContributor 的实现类
namespace Volo.Abp.Data;
//TODO: Create a Volo.Abp.Data.Seeding namespace?
public class DataSeeder : IDataSeeder, ITransientDependency
{
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected AbpDataSeedOptions Options { get; }
public DataSeeder(
//数据种子贡献器配置类
IOptions<AbpDataSeedOptions> options,
IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
Options = options.Value;
}
[UnitOfWork]
public virtual async Task SeedAsync(DataSeedContext context)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
//DataSeedContext 配置上下文类
if (context.Properties.ContainsKey(DataSeederExtensions.SeedInSeparateUow))
{
var manager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
//只要加入 AbpDataSeedOptions 的贡献器类才会被执行
foreach (var contributorType in Options.Contributors)
{
var options = context.Properties.TryGetValue(
DataSeederExtensions.SeedInSeparateUowOptions, out var uowOptions)
? (AbpUnitOfWorkOptions) uowOptions!
: new AbpUnitOfWorkOptions();
var requiresNew = context.Properties.TryGetValue(
DataSeederExtensions.SeedInSeparateUowRequiresNew, out var obj) && (bool) obj!;
//开启一个工作单元,指定事务的参数
using (var uow = manager.Begin(options, requiresNew))
{
//获取贡献器类
var contributor = (IDataSeedContributor)scope.
ServiceProvider.GetRequiredService(contributorType);
await contributor.SeedAsync(context);
await uow.CompleteAsync();
}
}
}
else
{
//不执行工作单元
foreach (var contributorType in Options.Contributors)
{
var contributor = (IDataSeedContributor)scope.
ServiceProvider.GetRequiredService(contributorType);
await contributor.SeedAsync(context);
}
}
}
}
}
👍🎉🎊