在创建实体类、实体配置类和 DbContext 类之后,接下来就需要生成数据库了。在 EFCore 中,数据库都是根据代码生成的,而不是程序员创建的,即程序员先创建实体,然后通过工具生成数据库,在生成过程中各个实体被映射到数据库的各个表,各个实体的属性被映射到各个表的数据列。

生成数据库之前,需要安装 Microsoft.EntityFrameworkCore.Tools 包,然后在控制台当前项目下执行以下两行命令即可:

Add-Migration Init
Update-Database

执行后,工具会根据 DbContext 中的配置连接对应的数据库,然后生成对应的数据库和表。上述的 Book 实体生成的 SQLServer 数据表设计如下

Migration

上面生成数据库所使用的工具就是 Migration,在 EFCore 中数据库设计的改变是通过 Migration 来实现的。Migration 的中文意思是”迁移“,但在 EFCore 里它具有更广泛的意思:程序员在开发过程中,可能对实体类进行修改,比如增加实体类或增加实体类的属性,这相当于在数据库中增加一张表或者在某张表中增加一列,对数据库的一次修改,称为一次 Migration。

例如上面通过 Add-Migration 命令就提交了一次名为 Init 的 Migration,它将会告诉 EFCore 去创建数据库并增加 Book 实体对应的数据表,虽然这是从无到有,但 EFCore 也认为它属于 Migration 的一种。

假设此时对 Book 实体进行修改,在 BookConfig 中添加如下代码:

class BookConfig : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable("T_Books");
        builder.Property(e => e.Title).HasMaxLength(50).IsRequired();
        builder.Property(e => e.Author).HasMaxLength(20).IsRequired();
    }
}

这将修改实体的 Title 属性和 Author 属性,使它们分别具有 50 和 20 的最大长度,并且都不可为空。此时实体配置类修改了,需要提交一次 Migration,通过 Add-Migration 命令,后面接此 Migration 的名字:

Add-Migration AddMaxLen

我们如何浏览已经提交的 Migration 呢?其实,在每次提交 Migration 时,EFCore 都会在当前项目下的 Migrations 目录创建对应的数据库设计代码文件,这些代码就是用来实现数据库设计的更新的。例如这次 AddMaxLen 的 代码:

using Microsoft.EntityFrameworkCore.Migrations;

namespace EFCore.Migrations
{
    public partial class AddMaxLen : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AlterColumn<string>(
                name: "Title",
                table: "T_Books",
                type: "nvarchar(50)",
                maxLength: 50,
                nullable: false,
                defaultValue: "",
                oldClrType: typeof(string),
                oldType: "nvarchar(max)",
                oldNullable: true);

            migrationBuilder.AlterColumn<string>(
                name: "Author",
                table: "T_Books",
                type: "nvarchar(20)",
                maxLength: 20,
                nullable: false,
                defaultValue: "",
                oldClrType: typeof(string),
                oldType: "nvarchar(max)",
                oldNullable: true);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AlterColumn<string>(
                name: "Title",
                table: "T_Books",
                type: "nvarchar(max)",
                nullable: true,
                oldClrType: typeof(string),
                oldType: "nvarchar(50)",
                oldMaxLength: 50);

            migrationBuilder.AlterColumn<string>(
                name: "Author",
                table: "T_Books",
                type: "nvarchar(max)",
                nullable: true,
                oldClrType: typeof(string),
                oldType: "nvarchar(20)",
                oldMaxLength: 20);
        }
    }
}

当然开发人员并不需要编写这些代码,它们都是通过工具生成的,只需要知道每一个 Migration 都有一个 Up 和 Down 的方法,它们分别是这次数据库修改的代码和撤销修改的代码。注意这个时候数据库并没有发送变化,因为提交 Migration 只是让 EFCore 生成了代码,并没有执行。接下来要更新数据库的话需要再执行:

Update-Database

这行命令会让 EFCore 将数据库更新到最新的 Migration。

实际上,Migration 机制有点类似于 Git 的版本控制,每一次数据库设计的修改,都通过一次 Migration 来表示,而 Migration 其实就是一个个 CS 代码文件,每个代码文件中都将这次数据库设计的修改翻译为了 C# 代码,之后通过 Update-Database 命令来执行这些代码,并更新数据库。EFCore 也会同步创建一个名为 __EFMigrationsHistory 的数据库,这个数据库储存了当前程序数据库处于哪个 Migration 状态。

Migration 不仅支持更新,也支持回退,通过

Update-Database XXX

可以让数据库回滚或者更新(这取决于当前数据库处于哪个 Migration)到名为 XXX 的 Migration。

在使用 Update-Database 命令时,EFCore 首先会到 __EFMigrationsHistory 数据库查看当前程序数据库处于哪个 Migration,然后判断这个 Migration 在 XXX 之前还是在 XXX 之后,如果在 XXX 之前,那么 EFCore 就会执行当前 Migration 到 XXX 的所有 Migration 的 Up 方法,这样就实现了数据库更新。如果在 XXX 之后,那么 EFCore 就会执行当前 Migration 到 XXX 的所有 Migration 的 Down 方法,这样就实现了数据库回滚。

通过 Remove-Migration 命令也可以撤销最近提交的一次 Migration,但这只是删除了 Migration,并不会导致数据库发生变化,如果需要撤销数据库中已经发生的更改,应该使用 Update-DataBase 进行回滚。

使用 EFCore,开发人员便只通过 Migration 进行数据库设计的更改,如果此时手动修改数据库设计,可能会导致 Migration 机制无法正常执行。