You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
abp/docs/zh-Hans/Entity-Framework-Core-Migra...

14 KiB

EF核心高级数据库迁移

本文首先介绍应用程序启动模板提供的默认结构,并讨论您可能希望为自己的应用程序实现的各种场景.

本文档适用于希望完全理解和自定义应用程序启动模板附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循启动教程.

关于EF Core 代码优先迁移

Entity Framework Core 提供了一种简单强大数据库迁移系统. ABP框架启动模板使用这个系统,让你以标准的方式开发你的应用程序.

但是EF Core迁移系统在[模块化环境中不是很好],在模块化环境中,每个模块都维护自己的数据库架构,而实际上两个或多个模块可以共享一个数据库.

由于ABP框架在所有方面都关心模块化,所以它为这个问题提供了解决方案. 如果你需要自定义数据库结构,那么应当了解这个解决方案.

参阅EF Core文档充分了解EF Core Code First迁移,以及为什么需要这样的系统.

默认解决方案与数据库配置

当你创建一个新的Web应用程序(使用EF Core,它是默认的数据库提供程序),你的解决方案结构类似下图:

bookstore-visual-studio-solution-v3

实际的解决方案结构可能会根据你的偏好有所不同,但是数据库部分是相同的.

数据库架构

启动模板已预安装了一些应用程序模块. 解决方案的每一层都有相应的模块包引用. 所以 .EntityFrameworkCore 项目含有使用 EntityFrameworkCore 模块的Nuget的引用:

bookstore-efcore-dependencies

通过这种方式,你可以看到所有的.EntityFrameworkCore项目下的EF Core的依赖.

除了模块引用之外,它还引用了 Volo.Abp.EntityFrameworkCore.SqlServer 包,因为启动模板预配置的是Sql Server. 参阅文档了解如何切换到其它DBMS.

虽然每个模块在设计上有自己的DbContext类,并且可以使用其自己的物理数据库,但解决方案的配置是使用单个共享数据库如下图所示:

single-database-usage

这是最简单的配置,适用于大部分的应用程序. appsettings.json 文件有名为Default单个连接字符串:

"ConnectionStrings": {
  "Default": "..."
}

所以你有一个单一的数据库模式,其中包含共享此数据库的模块的所有表.

ABP框架的连接字符串系统允许你轻松为所需的模块设置不同的连接字符串:

"ConnectionStrings": {
  "Default": "...",
  "AbpAuditLogging": "..."
}

示例配置告诉ABP框架审计日志模块应使用第二个连接字符串.

然而这仅仅只是开始. 你还需要创建第二个数据库以及里面审计日志表并使用code frist的方法维护数据库表. 本文档的主要目的之一就是指导你了解这样的数据库分离场景.

模块表

每个模块都使用自己的数据库表. 例如身份模块有一些表来管理系统中的用户和角色.

表前缀

由于所有模块都允许共享一个数据库(这是默认配置),所以模块通常使用前缀来对自己的表进行分组.

基础模块(如身份, 租户管理审计日志)使用 Abp 前缀, 其他的模块使用自己的前缀. 如Identity Server 模块使用前缀 IdentityServer.

如果你愿意,你可以为你的应用程序的模块更改数据库表前缀. 例:

Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids";

这段代码更改了Identity Server的前缀. 在应用程序的最开始编写这段代码.

每个模块还定义了 DbSchema 属性,你可以在支持schema的数据库中使用它.

项目

从数据库的角度来看.有三个重要的项目将在下一节中解释.

.EntityFrameworkCore 项目

这个项目有应用程序的 DbContext类(本例中的 BookStoreDbContex ).

每个模块都使用自己的 DbContext 类来访问数据库。同样你的应用程序有它自己的 DbContext. 通常在应用程序中使用这个 DbContet(如果你遵循最佳实践,应该在自定义仓储中使用). 它几乎是一个空的 DbContext,因为你的应用程序在一开始没有任何实体,除了预定义的 AppUser 实体:

[ConnectionStringName("Default")]
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<AppUser> Users { get; set; }

    /* Add DbSet properties for your Aggregate Roots / Entities here. */

    public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options)
        : base(options)
    {

    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        /* Configure the shared tables (with included modules) here */

        builder.Entity<AppUser>(b =>
        {
            //Sharing the same table "AbpUsers" with the IdentityUser
            b.ToTable("AbpUsers");

            //Configure base properties
            b.ConfigureByConvention();
            b.ConfigureAbpUser();

            //Moved customization of the "AbpUsers" table to an extension method
            b.ConfigureCustomUserProperties();
        });

        /* Configure your own tables/entities inside the ConfigureBookStore method */
        builder.ConfigureBookStore();
    }
}

这个简单的 DbContext 类仍然需要一些解释:

  • 它定义了一个 [connectionStringName] Attribute,它告诉ABP始终为此 Dbcontext 使用 Default 连接字符串.
  • 它从 AbpDbContext<T> 而不是标准的 DbContext 类继承. 你可以参阅EF Core集成文档了解更多. 现在你需要知道 AbpDbContext<T> 基类实现ABP框架的一些约定,为你自动化一些常见的任务.
  • 它为 AppUser 实体定义了 DbSet 属性. AppUser 与[身份模块]的 IdentityUser 实体共享同一个表(默认名为 AbpUsers). 启动模板在应用程序中提供这个实体,因为我们认为用户实体一般需要应用程序中进行定制.
  • 构造函数接受一个 DbContextOptions<T> 实例.
  • 它覆盖了 OnModelCreating 方法定义EF Core 映射.
    • 首先调用 base.OnModelCreating 方法让ABP框架为我们实现基础映射.
    • 然后它配置了 AppUser 实体的映射. 这个实体有一个特殊的情况(它与Identity模块共享一个表),在下一节中进行解释.
    • 最后它调用 builder.ConfigureBookStore() 扩展方法来配置应用程序的其他实体.

在介绍其他数据库相关项目之后,将更详细地说明这个设计.

.EntityFrameworkCore.DbMigrations 项目

正如前面所提到的,每个模块(和你的应用程序)有它们自己独立的 DbContext 类. 每个 DbContext 类只定义了自身模块的实体到表的映射,每个模块(包括你的应用程序)在运行时都使用相关的 DbContext 类.

如你所知,EF Core Code First迁移系统依赖于 DbContext 类来跟踪和生成Code First迁移. 那么我们应该使用哪个 DbContext 进行迁移? 答案是它们都不是. .EntityFrameworkCore.DbMigrations 项目中定义了另一个 DbContext (示例解决方案中的 BookStoreMigrationsDbContext).

MigrationsDbContext

MigrationsDbContext 仅用于创建和应用数据库迁移. 不在运行时使用. 它将所有使用的模块的所有实体到表的映射以及应用程序的映射合并.

通过这种方式你可以创建和维护单个数据库迁移路径. 然而这种方法有一些困难,接下来的章节将解释ABP框架如何克服这些困难. 首先以 BookStoreMigrationsDbContext 类为例:

/* This DbContext is only used for database migrations.
 * It is not used on runtime. See BookStoreDbContext for the runtime DbContext.
 * It is a unified model that includes configuration for
 * all used modules and your application.
 */
public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbContext>
{
    public BookStoreMigrationsDbContext(
        DbContextOptions<BookStoreMigrationsDbContext> options)
        : base(options)
    {

    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        /* Include modules to your migration db context */
        builder.ConfigurePermissionManagement();
        builder.ConfigureSettingManagement();
        builder.ConfigureBackgroundJobs();
        builder.ConfigureAuditLogging();
        builder.ConfigureIdentity();
        builder.ConfigureIdentityServer();
        builder.ConfigureFeatureManagement();
        builder.ConfigureTenantManagement();

        /* Configure customizations for entities from the modules included  */
        builder.Entity<IdentityUser>(b =>
        {
            b.ConfigureCustomUserProperties();
        });

        /* Configure your own tables/entities inside the ConfigureBookStore method */
        builder.ConfigureBookStore();
    }
}
共享映射代码

第一个问题是: 一个模块使用自己的 DbContext 这就需要到数据库的映射. 该 MigrationsDbContext 也需要相同的映射创建此模块的数据库表. 我们绝对不希望复制的映射代码.

解决方案是定义一个扩展方法(在ModelBuilder)由两个 DbContext 类调用. 所以每个模块都定义了这样的扩展方法.

For example, the builder.ConfigureBackgroundJobs() method call configures the database tables for the Background Jobs module. The definition of this extension method is something like that:

例如,builder.ConfigureBackgroundJobs() 方法调用[后台作业模块]配置数据库表. 扩展方法的定义如下:

public static class BackgroundJobsDbContextModelCreatingExtensions
{
    public static void ConfigureBackgroundJobs(
        this ModelBuilder builder,
        Action<BackgroundJobsModelBuilderConfigurationOptions> optionsAction = null)
    {
        var options = new BackgroundJobsModelBuilderConfigurationOptions(
            BackgroundJobsDbProperties.DbTablePrefix,
            BackgroundJobsDbProperties.DbSchema
        );

        optionsAction?.Invoke(options);

        builder.Entity<BackgroundJobRecord>(b =>
        {
            b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema);

            b.ConfigureCreationTime();
            b.ConfigureExtraProperties();

            b.Property(x => x.JobName)
                .IsRequired()
                .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength);

            //...
        });
    }
}

此扩展方法还获取选项用于更改此模块的数据库表前缀和模式,但在这里并不重要.

最终的应用程序在 MigrationsDbContext 类中调用扩展方法, 因此它可以确定此 MigrationsDbContext 维护的数据库中包含哪些模块. 如果要创建第二个数据库并将某些模块表移动到第二个数据库,则需要有第二个MigrationsDbContext 类,该类仅调用相关模块的扩展方法. 下一部分将详细介绍该主题.

同样 ConfigureBackgroundJobs 方法也被后台作业模块的 DbContext 调用:

[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)]
public class BackgroundJobsDbContext
    : AbpDbContext<BackgroundJobsDbContext>, IBackgroundJobsDbContext
{
    public DbSet<BackgroundJobRecord> BackgroundJobs { get; set; }

    public BackgroundJobsDbContext(DbContextOptions<BackgroundJobsDbContext> options) 
        : base(options)
    {

    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        //Reuse the same extension method!
        builder.ConfigureBackgroundJobs();
    }
}

以这种方式,可以在 DbContext 类之间共享模块的映射配置.

重用模块的表

您可能想在应用程序中重用依赖模块的表. 在这种情况下你有两个选择:

  1. 你可以直接使用模块定义的实体.
  2. 你可以创建一个新的实体映射到同一个数据库表。
使用由模块定义的实体

使用实体定义的模块有标准用法非常简单. 例如身份模块定义了 IdentityUser 实体. 你可以为注入 IdentityUser 仓储,为此实体执行标准仓储操作. 例:

using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;

namespace Acme.BookStore
{
    public class MyService : ITransientDependency
    {
        private readonly IRepository<IdentityUser, Guid> _identityUserRepository;

        public MyService(IRepository<IdentityUser, Guid> identityUserRepository)
        {
            _identityUserRepository = identityUserRepository;
        }

        public async Task DoItAsync()
        {
            //Get all users
            var users = await _identityUserRepository.GetListAsync();
        }
    }
}

示例注入了 IRepository<IdentityUser,Guid>(默认仓储). 它定义了标准的存储库方法并实现了 IQueryable 接口.

另外,身份模块定义了 IIdentityUserRepository(自定义仓储) 你的应用程序也可以注入和使用它. IIdentityUserRepositoryIdentityUser 实体提供了额外的定制方法,但它没有实现 IQueryable.

创建一个新的实体

TODO

讨论另一种场景:每个模块管理自己的迁移路径

TODO