EF Core 数据库迁移
本文档首先介绍 应用程序启动模板 提供的默认结构,然后讨论您可能想为自己应用程序实现的各种场景。
本文档适用于希望全面理解并自定义随 应用程序启动模板 而来的数据库结构的开发者。如果您只是想创建实体并管理您的Code First迁移,只需遵循 入门教程 即可。
源代码
您可以在 此处 找到本文档引用的示例项目源代码。但是,您需要阅读并理解本文档,才能理解该示例项目的源代码。
关于 EF Core Code First 迁移
Entity Framework Core 提供了一个易用且强大的 数据库迁移系统 。ABP 启动模板 利用此系统的优势,允许您以标准方式开发应用程序。
然而,EF Core 迁移系统在模块化环境中表现不佳,在这种环境中,每个模块维护其自己的数据库模式,而实际上两个或多个模块可能共享一个数据库。
由于 ABP 在所有方面都注重模块化,它为此问题提供了一个解决方案。如果您需要自定义数据库结构,理解此解决方案非常重要。
请参阅 EF Core 官方文档 以全面了解 EF Core Code First 迁移以及为什么需要这样的系统。
默认解决方案与数据库配置
当您 创建一个新的 Web 应用程序(使用 EF Core,这是默认的数据库提供程序)时,您的解决方案结构将类似于下图:
根据您的偏好,实际的解决方案结构可能略有不同,但数据库部分将保持不变。
本文档将使用
Acme.BookStore示例项目名称来指代项目和类。您需要在您的解决方案中找到对应的类/项目。
数据库结构
启动模板预安装了一些 应用程序模块。解决方案的每一层都有对应模块的包引用。因此,.EntityFrameworkCore 项目引用了所使用模块的 .EntityFrameworkCore 包的 NuGet 引用:
通过这种方式,您将所有EF Core 依赖项收集到 .EntityFrameworkCore 项目下。
除了模块引用外,它还引用了
Volo.Abp.EntityFrameworkCore.SqlServer包,因为启动模板已为 SQL Server 进行了预配置。如果您想 切换到另一个 DBMS ,请参阅文档。
虽然每个模块按设计都有自己的 DbContext 类,并且可以使用其自己的物理数据库,但解决方案配置为使用单个共享数据库,如下图所示:
这是最简单的配置,适用于大多数应用程序。appsettings.json 文件有一个单一的连接字符串,名为 Default:
"ConnectionStrings": {
"Default": "..."
}
因此,您有一个单一的数据库模式,其中包含共享此数据库的所有模块的表。
ABP 的 连接字符串 系统允许您轻松地为所需模块设置不同的连接字符串:
"ConnectionStrings": {
"Default": "...",
"AbpAuditLogging": "..."
}
示例配置告诉 ABP 为 审计日志模块 使用第二个连接字符串(如果您没有为模块指定连接字符串,则使用 Default 连接字符串)。
但是,只有当具有给定连接字符串的审计日志数据库可用时,这才能工作。因此,您需要创建第二个数据库,在其中创建审计日志表并维护这些数据库表。如果您手动完成所有这些操作,则没有问题。然而,推荐的方法是 Code First 迁移。本文档的主要目的之一就是指导您处理此类数据库分离场景。
模块表
每个模块都使用其自己的数据库表。例如,身份模块 有一些表来管理系统中的用户和角色。
表前缀
由于允许所有模块共享单个数据库(这是默认配置),模块通常使用表名前缀来分组自己的表。
基础模块,如 身份、租户管理 和 审计日志,使用 Abp 前缀,而其他一些模块使用自己的前缀。例如,OpenIddict 模块使用 OpenIddict 前缀。
如果您愿意,可以为应用程序中的模块更改数据库表名前缀。示例:
Volo.Abp.OpenIddict.AbpOpenIddictDbProperties.DbTablePrefix = "Auth";
此代码更改了 OpenIddict 模块的前缀。请将此代码写在应用程序的最开头。
每个模块还定义了
DbSchema属性(靠近DbTablePrefix),因此您可以为支持模式使用的数据库设置它。
.EntityFrameworkCore 项目
解决方案包含一个名称以 .EntityFrameworkCore 结尾的项目。该项目包含应用程序的 DbContext 类(本示例为 BookStoreDbContext)。
每个模块使用自己的 DbContext 类来访问数据库。同样,您的应用程序也有自己的 DbContext。您通常会在应用程序代码中使用此 DbContext(如果您遵循最佳实践并将数据访问代码隐藏在存储库后面,则在 存储库 中使用)。它几乎是一个空的 DbContext,因为您的应用程序在开始时没有任何实体:
[ReplaceDbContext(typeof(IIdentityDbContext))]
[ReplaceDbContext(typeof(ITenantManagementDbContext))]
[ConnectionStringName("Default")]
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
/* 在此处为您的聚合根 / 实体添加 DbSet 属性 */
/* 用于来自被替换的 DbContext 的实体的 DbSet */
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* 将模块包含到您的迁移数据库上下文中 */
builder.ConfigurePermissionManagement();
builder.ConfigureSettingManagement();
builder.ConfigureBackgroundJobs();
builder.ConfigureAuditLogging();
builder.ConfigureIdentity();
builder.ConfigureIdentityServer();
builder.ConfigureFeatureManagement();
builder.ConfigureTenantManagement();
/* 在此处配置您自己的表/实体。示例: */
//builder.Entity<YourEntity>(b =>
//{
// b.ToTable("YourEntities");
// b.ConfigureByConvention(); // 为基类属性自动配置
// //...
//});
}
}
这个 DbContext 类需要一些解释:
- 它分别为
IIdentityDbContext和ITenantManagementDbContext定义了[ReplaceDbContext]属性,这些属性在运行时用您的DbContext替换了身份和租户管理模块的DbContext。这使我们能够轻松地通过将您的实体与来自这些模块的实体(通过存储库)进行连接来执行 LINQ 查询。 - 它定义了一个
[ConnectionStringName]属性,告诉 ABP 始终为此DbContext使用Default连接字符串。 - 它继承自
AbpDbContext<T>,而不是标准的DbContext类。您可以查看 EF Core 集成 文档了解更多信息。目前,只需了解AbpDbContext<T>基类实现了 ABP 的一些约定,为您自动执行一些常见任务。 - 它为来自被替换
DbContext的实体(通过实现相应的接口)声明了DbSet属性。为了简洁起见,这些DbSet属性未在上面显示,但您可以在应用程序代码的某个region中找到它们。 - 构造函数接受一个
DbContextOptions<T>实例。 - 它重写了
OnModelCreating方法来定义 EF Core 映射。- 它首先调用
base.OnModelCreating方法,让 ABP 为我们实现基础映射。 - 然后它为使用的模块调用一些
builder.ConfigureXXX()方法。这使得可以将这些模块的数据库映射添加到此DbContext中,因此当我们添加新的 EF Core 数据库迁移时,它会创建模块的数据库表。 - 您可以按照示例代码中的注释配置您自己实体的映射。此时,您还可以更改您正在使用的模块的映射。
- 它首先调用
讨论替代场景:每个模块管理自己的迁移路径
如前所述,在 .EntityFrameworkCore 项目中,我们合并所有模块(加上您应用程序的映射)的所有数据库映射,以创建一个统一的迁移路径。
另一种方法是允许每个模块拥有自己的迁移来维护其数据库表。虽然一开始看起来更具模块化,但在实践中存在一些缺点:
- EF Core 迁移系统依赖于 DBMS 提供程序。例如,如果一个模块为 SQL Server 创建了迁移,那么您就不能将此迁移代码用于 MySQL。对于一个模块来说,维护所有可用 DBMS 提供程序的迁移是不现实的。将迁移留给应用程序代码(如本文档所述)允许您在应用程序代码中选择 DBMS。如果您在模块中可以依赖特定的 DBMS,这对您来说不是问题,但所有预构建的 ABP 模块都是与 DBMS 无关的。
- 在最终应用程序中,自定义/增强映射和生成的迁移代码会更加困难。
- 当您使用多个模块时,跟踪和应用更改到数据库会更加困难。
使用多个数据库
默认的启动模板被组织为所有模块和您的应用程序使用单个数据库。然而,ABP 和所有预构建模块的设计使得它们可以使用多个数据库。每个模块可以使用自己的数据库,或者您可以将模块分组到少数几个数据库中。
本节将解释如何将审计日志、设置管理和权限管理模块表移动到第二个数据库,而其余模块继续使用主("Default")数据库。
最终结构将如下图所示:
更改连接字符串部分
第一步是更改所有 appsettings.json 文件中的连接字符串部分。初始状态如下:
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
将其更改为如下所示:
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True"
}
为相关模块添加了三个额外的连接字符串,指向 BookStore_SecondDb 数据库(它们都相同)。例如,AbpPermissionManagement 是权限管理模块使用的连接字符串名称。
AbpPermissionManagement 是权限管理模块 定义 的常量。如果您定义了连接字符串,ABP 连接字符串选择系统 会为权限管理模块选择此连接字符串。如果您未定义,则回退到 Default 连接字符串。
创建第二个 DbContext
如上所述定义连接字符串在运行时就足够了。但是,BookStore_SecondDb 数据库尚不存在。您需要为相关模块创建数据库和表。
就像主数据库一样,我们希望使用 EF Core Code First 迁移系统来创建和维护第二个数据库。因此,在 .EntityFrameworkCore 项目中创建一个新的 DbContext 类:
using Microsoft.EntityFrameworkCore;
using Volo.Abp.AuditLogging.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.PermissionManagement.EntityFrameworkCore;
using Volo.Abp.SettingManagement.EntityFrameworkCore;
namespace BookStore.EntityFrameworkCore
{
[ConnectionStringName("AbpPermissionManagement")]
public class BookStoreSecondDbContext :
AbpDbContext<BookStoreSecondDbContext>
{
public BookStoreSecondDbContext(
DbContextOptions<BookStoreSecondDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* 将模块包含到您的迁移数据库上下文中 */
builder.ConfigurePermissionManagement();
builder.ConfigureSettingManagement();
builder.ConfigureAuditLogging();
}
}
}
[ConnectionStringName(...)]属性在这里很重要,它告诉 ABP 应该为此DbContext使用哪个连接字符串。我们使用了AbpPermissionManagement,但所有连接字符串都相同。
我们需要将此 BookStoreSecondDbContext 类注册到依赖注入系统中。打开 BookStore.EntityFrameworkCore 项目中的 BookStoreEntityFrameworkCoreModule 类,并将以下行添加到 ConfigureServices 方法中:
context.Services.AddAbpDbContext<BookStoreSecondDbContext>();
我们还应该创建一个 设计时 Db Factory 类,供 EF Core 工具(例如 Add-Migration 和 Update-Database PCM 命令)使用:
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace BookStore.EntityFrameworkCore
{
/* 此类为 EF Core 控制台命令所需
*(例如 Add-Migration 和 Update-Database 命令) */
public class BookStoreSecondDbContextFactory
: IDesignTimeDbContextFactory<BookStoreSecondDbContext>
{
public BookStoreSecondDbContext CreateDbContext(string[] args)
{
var configuration = BuildConfiguration();
var builder = new DbContextOptionsBuilder<BookStoreSecondDbContext>()
.UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement"));
return new BookStoreSecondDbContext(builder.Options);
}
private static IConfigurationRoot BuildConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../BookStore.DbMigrator/"))
.AddJsonFile("appsettings.json", optional: false);
return builder.Build();
}
}
}
现在,您可以打开包管理器控制台,选择 .EntityFrameworkCore 项目作为默认项目(确保 .Web 项目仍然是启动项目),然后运行以下命令:
Add-Migration "Initial" -OutputDir "SecondDbMigrations" -Context BookStoreSecondDbContext
这将在 .EntityFrameworkCore 项目中添加一个 SecondDbMigrations 文件夹,并在其中创建一个迁移类。OutputDir 和 Context 参数是必需的,因为我们在同一个项目中目前有两个 DbContext 类和两个迁移文件夹。
现在,您可以运行以下命令来创建数据库及其中的表:
Update-Database -Context BookStoreSecondDbContext
应创建一个名为 BookStore_SecondDb 的新数据库。
从主数据库中移除模块
我们已经创建了第二个数据库,其中包含审计日志、权限管理和设置管理模块的表。因此,我们应该从主数据库中删除这些表。这非常容易。
首先,从 BookStoreDbContext 类中移除以下行:
builder.ConfigurePermissionManagement();
builder.ConfigureSettingManagement();
builder.ConfigureAuditLogging();
打开包管理器控制台,选择 .EntityFrameworkCore 作为默认项目(确保 .Web 项目仍然是启动项目),然后运行以下命令:
Add-Migration "Removed_Audit_Setting_Permission_Modules" -Context BookStoreDbContext
此命令将创建一个新的迁移类,如下所示:
public partial class Removed_Audit_Setting_Permission_Modules : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AbpAuditLogActions");
migrationBuilder.DropTable(
name: "AbpEntityPropertyChanges");
migrationBuilder.DropTable(
name: "AbpPermissionGrants");
migrationBuilder.DropTable(
name: "AbpSettings");
migrationBuilder.DropTable(
name: "AbpEntityChanges");
migrationBuilder.DropTable(
name: "AbpAuditLogs");
}
...
}
在此步骤中要小心:
- 如果您有生产系统,那么您应该注意数据丢失。您需要在删除表之前将表内容移动到第二个数据库。
- 如果您尚未启动项目,您可以考虑删除所有迁移并重新创建初始迁移,以获得更清晰的迁移历史记录。
运行以下命令从您的主数据库中删除表:
Update-Database -Context BookStoreDbContext
请注意,如果您尚未将其复制到新数据库,那么您还删除了一些初始种子数据(例如,管理员角色的权限授予)。如果您运行应用程序,可能无法再登录。解决方案很简单:重新运行解决方案中的 .DbMigrator 控制台应用程序,它将为新数据库播种。
自动化第二个数据库模式迁移
.DbMigrator 控制台应用程序可以在多个数据库上运行数据库种子代码,无需任何额外配置。但是,它无法为 BookStoreSecondDbContext 的数据库应用 EF Core Code First 迁移。现在,您将看到如何配置控制台迁移应用程序以处理两个数据库。
Acme.BookStore.EntityFrameworkCore 项目中的 EntityFrameworkCoreBookStoreDbSchemaMigrator 类负责为 BookStoreMigrationsDbContext 迁移数据库模式。它应该如下所示:
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using BookStore.Data;
using Volo.Abp.DependencyInjection;
namespace BookStore.EntityFrameworkCore
{
public class EntityFrameworkCoreBookStoreDbSchemaMigrator
: IBookStoreDbSchemaMigrator, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
public EntityFrameworkCoreBookStoreDbSchemaMigrator(
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task MigrateAsync()
{
/* 我们特意从 IServiceProvider 解析 BookStoreDbContext
*(而不是直接注入它)
* 以正确获取当前作用域中当前租户的连接字符串。
*/
await _serviceProvider
.GetRequiredService<BookStoreDbContext>()
.Database
.MigrateAsync();
}
}
}
在 MigrateAsync 方法内部添加以下代码:
await _serviceProvider
.GetRequiredService<BookStoreSecondDbContext>()
.Database
.MigrateAsync();
因此,MigrateAsync 方法应如下所示:
public async Task MigrateAsync()
{
/* 我们特意从 IServiceProvider 解析 BookStoreDbContext
*(而不是直接注入它)
* 以正确获取当前作用域中当前租户的连接字符串。
*/
await _serviceProvider
.GetRequiredService<BookStoreDbContext>()
.Database
.MigrateAsync();
await _serviceProvider
.GetRequiredService<BookStoreSecondDbContext>()
.Database
.MigrateAsync();
}
就这样。现在您可以运行 .DbMigrator 应用程序来迁移和播种数据库。要进行测试,您可以删除两个数据库,然后再次运行 .DbMigrator 应用程序,查看它是否同时创建了两个数据库。
修复测试
创建新的 DbContext 会破坏集成测试。修复起来很容易。打开 BookStore.EntityFrameworkCore.Tests 项目中的 BookStoreEntityFrameworkCoreTestModule 类,找到 CreateDatabaseAndGetConnection 方法。它应该如下所示:
private static SqliteConnection CreateDatabaseAndGetConnection()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<BookStoreDbContext>()
.UseSqlite(connection)
.Options;
using (var context = new BookStoreDbContext(options))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
return connection;
}
将其更改为如下:
private static SqliteConnection CreateDatabaseAndGetConnection()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<BookStoreDbContext>()
.UseSqlite(connection)
.Options;
using (var context = new BookStoreDbContext(options))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
// 添加以下代码 --------------
var optionsForSecondDb = new DbContextOptionsBuilder<BookStoreSecondDbContext>()
.UseSqlite(connection)
.Options;
using (var context = new BookStoreSecondDbContext(optionsForSecondDb))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
//--------------------------------------
return connection;
}
集成测试现在将正常工作。为了简单起见,我在测试中使用了相同的数据库。
分离主机和租户数据库模式
在多租户解决方案中,您可能希望分离数据库模式,以便当租户拥有独立的数据库时,与主机相关的表不会位于租户数据库中。
一些预构建的 ABP 模块仅与主机端相关,例如 租户管理 模块。因此,在租户的 DbContext 类中,您不调用 modelBuilder.ConfigureTenantManagement(),仅此而已。
有些模块,如 身份 模块,在主机端和租户端都使用。它将租户用户存储在租户数据库中,将主机用户存储在主机数据库中。但是,它仅将一些实体(如 IdentityClaimType)存储在主机端。在这种情况下,您不希望将这些表添加到租户数据库中,即使它们未被使用并且对于租户来说将始终为空。
ABP 提供了一种简单的方法来为 DbContext 设置多租户端,以便模块可以检查它并决定是否将表映射到数据库。
public class MyTenantDbContext : AbpDbContext<MyTenantDbContext>
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.SetMultiTenancySide(MultiTenancySides.Tenant);
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureIdentity();
modelBuilder.ConfigureFeatureManagement();
modelBuilder.ConfigureAuditLogging();
}
}
OnModelCreating 中的第一行将多租户端设置为 Tenant。对于此示例,功能管理表不会被创建(因为所有表都是主机特定的),因此调用 modelBuilder.ConfigureFeatureManagement() 没有效果。此外,ConfigureIdentity() 调用会尊重多租户端,并且不会为此数据库创建主机特定的表。
SetMultiTenancySide 可以获取以下值:
MultiTenancySides.Both(默认值): 此DbContext(以及相关数据库)由主机和租户共享。MultiTenancySides.Host: 此DbContext(以及相关数据库)仅由主机端使用。MultiTenancySides.Tenant: 此DbContext(以及相关数据库)仅用于租户。
如果您创建可重用的应用程序模块或想在应用程序代码中检查该值,可以使用 modelBuilder.GetMultiTenancySide() 来检查当前端。
var side = modelBuilder.GetMultiTenancySide();
if (!side.HasFlag(MultiTenancySides.Host))
{
...
}
或者,实际上您可以使用其中一个快捷扩展方法:
if (modelBuilder.IsTenantOnlyDatabase())
{
...
}
有四种方法可以检查当前端:
IsHostDatabase(): 如果您应该创建主机相关的表,则返回true。相当于检查modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Host)。IsHostOnlyDatabase(): 如果您应该仅创建主机相关的表,但不应该创建租户相关的表,则返回true。相当于检查modelBuilder.GetMultiTenancySide() == MultiTenancySides.Host。IsTenantDatabase(): 如果您应该创建租户相关的表,则返回true。相当于检查modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Tenant)。IsTenantOnlyDatabase(): 如果您应该仅创建租户相关的表,但不应该创建主机相关的表,则返回true。相当于检查modelBuilder.GetMultiTenancySide() == MultiTenancySides.Tenant。
所有预构建的 ABP 模块 在其 modelBuilder.ConfigureXXX() 方法中都会检查此值。
结论
本文档解释了如何拆分数据库并管理 Entity Framework Core 解决方案的数据库迁移。简而言之,您需要为不同的数据库准备单独的迁移项目。
抠丁客






