ABP abp detail
ABP abp detail
2023/6/1
➡️

数据库迁移

数据库上下文

数据库迁移时上下文的来源:

  1. 指定启动项目,在启动项目的服务容器里获取数据库上下文
  2. 无法在服务容器 DI 里获取,者尝试通过无参构造器生成数据库上下文
  3. 如果存在 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);
            }
        }
      }
  }
}
👍🎉🎊