目录
一、数据库迁移原理
数据库迁移的使用看似很简单,但是内部实现非常复杂,只有了解它的内部实现原理,我们才能更好地使用它。
Migrations 文件夹下的内容都是数据库迁移生成的代码,这些代码记录了对数据库的修改操作,一般情况下我们无须手工修改这些代码。这些代码由两部分组成,一部分是以数字开头的文件,每一个文件代表一次对数据库的修改操作;另一部分是 ModelSnapshot.cs 文件,它是当前状态的快照。
我们注意到,每次执行 Add-Migration 之后,Migrations 文件夹下都会生成两个文件,个文件的名字为“数字_迁移名字.cs”,另一个文件的名字为“数字_迁移名字Designer.cs”,我们把每一次执行 Add-Migration 称作一次“迁移”。这些以数字开头的一组文件就对应了一次迁移,这些迁移开头的数字就是迁移的版本号,这些版本号是递增的,因此我们根据版本号对其进行排序就能得知数据库迁移的历史。
使用迁移脚本,我们可以对当前连接的数据库执行版本号更高的迁移,这个操作叫作“向上迁移”,我们也可以执行把数据库回退到旧版本的迁移,这个操作叫“向下迁移”。假设项目中依次有版本号为 1001、1002、1003、1004、1005 的5个迁移,而当前连接的数据库已经完成的迁移版本号是 1003,那么在当前数据库上执行 1004 这个脚本以后,就完成了向上迁移;我们也可以把当前的数据库回退到 1002 这个版本,这样就完成了向下迁移。由于 EF Core 记录了全部的历史版本信息,因此我们还可以连续迁移,比如可以在当前迁移版本号为 1003 的数据库上向下迁移到 1002、再向下迁移到 1001。正因为如此,除非有特殊需要,否则我们不要删除Migrations 文件夹下的代码。
接下来,再详细看看每一组迁移中两个文件的作用。以 AddAuthor 为例,2201111223317AddAuthorcs 中记录的是和具体数据库无关的抽象模型,而 20201111223317 AddAuthorDesigner.cs 记录的是和具体数据库相关的代码。20201111223317_AddAuthor.cs 文件的主要内容如下代码所示。
public partial class AddAuthor : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(name:"Authors" columns: table => new
{ Id = table.Column<Guid>(nullable: false),
Name = table.Column<string>(nullable: true) }
constraints: table => table.PrimaryKey("PK Authors", x => x.Id));
}
protected override void Down (MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Authors");
}
}
AddAuthor 类中包含Up 和 Down 两个方法,Up 方法中定义的是向上迁移的代码,也就是把上一个版本的数据库迁移到这个版本要执行的代码,而 Down 方法中定义的则是向下迁移的代码,也就是把这个版本的数据库迁移回上一个旧版本的代码。这些代码都是由迁移工具生成的,一般不需要编写或者修改这些代码,但是为了研究 F Core 的原理,我们还是有必要看它的大概逻辑。
可以看到,Up 方法中,我们调用 CreateTable 方法创建了 Authors 表,并且定义了和实类中对应的列,而 Down 方法中则调用 DropTable 方法把 Authors 表删除。当我们在上~个本的数据库中执行这个迁移脚本的时候,Up 方法被执行,因此 Authors 表被创建,当我们要回退到上一个版本的数据库的时候,Down 方法就会被执行,因此 Authors 表被删除。因”Up方法是 Down 方法的“撒销操作”,所以这两个方法的代码需要实现完全反的操作,也然是 Down方法中的代码应该恰好把 Up 方法中对数据库的操作完全撤销,既不缺失一些操作也不多出额外的操作。
20201111223317_AddAuthor,Designer.cs 文件中定义的也是和 20201111223317_AddAuthor.cs中相同的AddAuthor类,它们两个通过部分类的语法各自组成AddAuthor 类的一部分。我们看-下20201111223317 AddAuthor.Designer.cs 文件,其主干内容如下代码所示。
[DbContext(typeof(TestDbContext))]
[Migration("20201111223317 AddAuthor")]
partial class AddAuthor{}
AddAuthor 类上添加的DbContext(typeof(TestDbContext)表示这个迁移脚本应用于哪个上下文,而[Migration("20201111223317 AddAuthor")]代表这个迁移脚本的版本号。
我们再查看一下数据库,会发现数据库中有一个 EFMigrationsHistory 表。
在表可以看到,_EFMigrationsHistory 表中记录的就是当前数据库曾经应用过的迁移脚本,是按照顺序排列的,最后一条数据就是数据库最后一次应用的迁移版本号。EF Core 就是基于张表得知当前连接的数据库的迁移版本号的,因此除非有特殊需要,否则不要修改这张表及数据。
由于数据库迁移工具需要调用代码编译后的 DLL 文件去执行数据库迁移逻辑,因此在行数据库迁移命令的时候,迁移工具会先尝试构建项目,如果项目构建失败,则迁移工作不继续执行。如果项目代码中有语法错误等会导致构建失败的代码,在执行 Add-Migration 等命令的时候,迁移工具就会提示“Build failed”的错误信息。
如果解决方案中有多个项目,在执行 Add-Migration 等命今的时候,一定要确认在[程序包管理器控制台]中选中的是要迁移的项目。
二、其他数据库迁移命令
除了Add-migration、Update-database 这两个常用命令之外,EF Core 还提供了其他一些数据库迁移命令。这些命令被使用的机会相对来讲比较少,这里只介绍常用的功能。
1.Update-database 其他参数
我们可以用 Update-database XXX 把数据库回滚到 XXX 迁移脚本之后的状态。注意,这个命令只把当前连接的数据库进行回滚,因此迁移脚本仍然存在。
2.删除迁移脚本
可以用 Remove-migration 命令删除最后一次的迁移脚本
3.生成迁移脚本
我们可以用 Update-database 命令执行迁移脚本来自动修改数据库,但是这种方式只适合在开发环境下使用,而不能用于生产环境。因为基于安全考虑,很多公司要求对生产环境数据库的操作必须要经过审计,而 EF Core 的迁移代码是一个二进制的程序,很难满足审计的要求而且大部分公司的开发环境并不能直接连接生产环境数据库。
EF Core 中提供了 Script-Migration 命令来根据迁移代码生成 SOL 脚本,比如在[程序包管理器控制台]中输入 Script-Migration并执行,一个包含完整的数据库操作脚本的SQL文件就会被创建和打开。这个脚本可以被提交给相关人员审计,然后在生产数据库中执行。
如果生产数据库已经处于某个迁移版本的状态了,那么我们可以生成这个版本到某个新版本的SQL脚本。比如当前数据库的前一版本是 D,通过如下命令可以生成版本D到版本F的
SQL 脚本:Script-Migration D F。
在 EF Core中,我们还可以使用 context.Database.Migrate0代码来对程序当前连接的数据库进行迁移。这种方式是直接在代码中完成数据库迁移,很多公司的安全审计要求提供的是明文的SQL语句,因此我们需要根据公司安全审计要求决定是采用生成迁移 SQL 脚本的方式,还是通过迁移程序的方式执行数据库迁移。
三、反向工程(慎用)
在推荐的 EF Core 使用方式是代码优先 (code first),也就是先创建实体类,然后根据实体类生成数据库表。但是在实际的项目开发中,有时候数据库表是已经存在的,这时我们就需要基于已有的数据库表来使用 EF Core。
EF Core 中提供了反向工程的功能,它可以根据数据库表来反向生成实体类。
在[程序包管理器控制台]中执行如下的命令:
Scaffold-DbContext iServer=.;Database=demol;Trusted Connection=True;!Microsoft.EntityFrameworkCore.SqlServer
上面的命令就会为SOLServer的demo1数据库中的所有表生成实体类及上下文类。
反向工程可以大大减少开发的工作量,但是由于所有的代码都是根据数据库生成的,因此生成的实体类也许并不能满足项目的要求。以上面生成的实体类为例,所有的数据库表名为“T实体类名称的复数形式”,而反向工程则是根据EF Core 默认的规则生成“TCat”这样的类名,而不是“Cat”这样的类名。因此反向工程生成的代码可能需要手动修改,而且我们也要对应地为其增加 Fluent API 或者 Data Annotation 的配置。
需要特别注意的是,如果我们再次运行反向工程工具,对生成文件所做的任何更改都将丢失因此,如果我们已经对之前生成的代码做了修改,那么再次运行反向工程的时候要谨慎。
有部分开发人员,把反向工程工具当成日常开发工具使用,也就是项目开发的时候,一直手动维护数据库表结构,包括新增表、新增列的操作都是直接在数据库上进行的,然后使用反向工程工具生成实体类的代码。这种方式是不推荐的,因为这不符合“模型驱动”的开发理念变成了传统的“数据库驱动”的开发方式,这对复杂项目的管理是一个“前期很方便,后期越来越杂乱”的灾难。