mirror of https://github.com/abpframework/abp.git
6 changed files with 326 additions and 0 deletions
@ -0,0 +1,320 @@ |
|||
|
|||
# EF核心高级数据库迁移 |
|||
|
|||
本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论您可能希望为自己的应用程序实现的**各种场景**. |
|||
|
|||
> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md). |
|||
|
|||
## 关于EF Core 代码优先迁移 |
|||
|
|||
Entity Framework Core 提供了一种简单强大[数据库迁移系统](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). ABP框架[启动模板](Startup-Templates/Index.md)使用这个系统,让你以标准的方式开发你的应用程序. |
|||
|
|||
但是EF Core迁移系统在[模块化环境中不是很好],在模块化环境中,每个模块都维护**自己的数据库架构**,而实际上两个或多个模块可以**共享一个数据库**. |
|||
|
|||
由于ABP框架在所有方面都关心模块化,所以它为这个问题提供了**解决方案**. 如果你需要**自定义数据库结构**,那么应当了解这个解决方案. |
|||
|
|||
> 参阅[EF Core文档](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)充分了解EF Core Code First迁移,以及为什么需要这样的系统. |
|||
|
|||
## 默认解决方案与数据库配置 |
|||
|
|||
当你[创建一个新的Web应用程序](https://abp.io/get-started)(使用EF Core,它是默认的数据库提供程序),你的解决方案结构类似下图: |
|||
|
|||
 |
|||
|
|||
> 实际的解决方案结构可能会根据你的偏好有所不同,但是数据库部分是相同的. |
|||
|
|||
### 数据库架构 |
|||
|
|||
启动模板已预安装了一些[应用程序模块](Modules/Index.md). 解决方案的每一层都有相应的模块包引用. 所以 `.EntityFrameworkCore` 项目含有使用 `EntityFrameworkCore` 模块的Nuget的引用: |
|||
|
|||
 |
|||
|
|||
通过这种方式,你可以看到所有的`.EntityFrameworkCore`项目下的EF Core的依赖. |
|||
|
|||
> 除了模块引用之外,它还引用了 `Volo.Abp.EntityFrameworkCore.SqlServer` 包,因为启动模板预配置的是Sql Server. 参阅文档了解如何[切换到其它DBMS](Entity-Framework-Core-Other-DBMS.md). |
|||
|
|||
虽然每个模块在设计上有自己的`DbContext`类,并且可以使用其自己的**物理数据库**,但解决方案的配置是使用**单个共享数据库**如下图所示: |
|||
|
|||
 |
|||
|
|||
这是**最简单的配置**,适用于大部分的应用程序. `appsettings.json` 文件有名为`Default`**单个连接字符串**: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "..." |
|||
} |
|||
```` |
|||
|
|||
所以你有一个**单一的数据库模式**,其中包含**共享**此数据库的模块的所有表. |
|||
|
|||
ABP框架的[连接字符串](Connection-Strings.md)系统允许你轻松为所需的模块**设置不同的连接字符串**: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "...", |
|||
"AbpAuditLogging": "..." |
|||
} |
|||
```` |
|||
|
|||
示例配置告诉ABP框架[审计日志模块](Modules/Audit-Logging.md)应使用第二个连接字符串. |
|||
|
|||
然而这仅仅只是开始. 你还需要创建第二个数据库以及里面审计日志表并使用code frist的方法维护数据库表. 本文档的主要目的之一就是指导你了解这样的数据库分离场景. |
|||
|
|||
#### 模块表 |
|||
|
|||
每个模块都使用自己的数据库表. 例如[身份模块](Modules/Identity.md)有一些表来管理系统中的用户和角色. |
|||
|
|||
#### 表前缀 |
|||
|
|||
由于所有模块都允许共享一个数据库(这是默认配置),所以模块通常使用前缀来对自己的表进行分组. |
|||
|
|||
基础模块(如[身份](Modules/Identity.md), [租户管理](Modules/Tenant-Management.md) 和 [审计日志](Modules/Audit-Logging.md))使用 `Abp` 前缀, 其他的模块使用自己的前缀. 如[Identity Server](Modules/IdentityServer.md) 模块使用前缀 `IdentityServer`. |
|||
|
|||
如果你愿意,你可以为你的应用程序的模块更改数据库表前缀. |
|||
例: |
|||
|
|||
````csharp |
|||
Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; |
|||
```` |
|||
|
|||
这段代码更改了[Identity Server](Modules/IdentityServer.md)的前缀. 在应用程序的最开始编写这段代码. |
|||
|
|||
> 每个模块还定义了 `DbSchema` 属性,你可以在支持schema的数据库中使用它. |
|||
|
|||
### 项目 |
|||
|
|||
从数据库的角度来看.有三个重要的项目将在下一节中解释. |
|||
|
|||
#### .EntityFrameworkCore 项目 |
|||
|
|||
这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ). |
|||
|
|||
每个模块都使用自己的 `DbContext` 类来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在自定义[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: |
|||
|
|||
````csharp |
|||
[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集成](Entity-Framework-Core.md)文档了解更多. 现在你需要知道 `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` 类为例: |
|||
|
|||
````csharp |
|||
/* 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](Modules/Background-Jobs.md). The definition of this extension method is something like that: |
|||
|
|||
例如,`builder.ConfigureBackgroundJobs()` 方法调用[后台作业模块]配置数据库表. 扩展方法的定义如下: |
|||
|
|||
````csharp |
|||
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` 调用: |
|||
|
|||
````csharp |
|||
[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` 仓储,为此实体执行标准仓储操作. |
|||
例: |
|||
|
|||
````csharp |
|||
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`(自定义仓储), 你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. |
|||
|
|||
###### 创建一个新的实体 |
|||
|
|||
TODO |
|||
|
|||
##### 讨论另一种场景:每个模块管理自己的迁移路径 |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# 身份管理模块 |
|||
|
|||
参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. |
|||
@ -0,0 +1,3 @@ |
|||
# 租户管理模块 |
|||
|
|||
TODO |
|||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 54 KiB |
Loading…
Reference in new issue