Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 13 章 理解迁移

作者:Adam Freeman
翻译:陈广
日期:2019-5-3


本章,我描述了迁移功能,它使得 Entity Framework Core 可以确保数据库反映应用程序中的数据模型,即使该模型发生了变化。这被称为代码行行(Code First),意思是从所需的数据模型类开始,并使用迁移来创建和管理数据库。(与之对应的是数据库先行 database first,我将在第17、18章讲述)。

本章我解释了如何创建并应用迁移,如何评估迁移的影响,以及如何使用命令行工具和 API 管理迁移。表 13-1 是迁移的简述。

表 13-1:迁移简述

问题 回答
它们是什么? 迁移是一组命令,用于准备 Entity Framework Core 应用程序所使用的数据库。它们用于创建数据库并与数据模型中的更改保持同步。
它们有何用途? 迁移使得用于存储应用程序数据的数据库的创建和维护过程变得自动化。没有迁移,您将不得不使用 SQL 语句创建数据库,并手动配置 Entity Framework Core 来使用数据库
如何使用它们 迁移使用dotnet ef命令行工具来创建和应用
是否有任何缺陷或限制? Entity Framework Core 在处理迁移时产生的错误消息并不总是清晰的。最常见的问题是创建迁移,而不是将它们应用到数据库中。
有没有其他选择? 首先创建数据库,然后创建用于实体框架核心的数据模型类。例子见第17章。

警告:本章中描述的许多命令改变了数据库的结构,并可能导致数据丢失。不要在生产数据库上使用这些命令,直到您确信了解了这些命令的效果,并且只在备份了关键数据之后才使用这些命令。

表13-2为本章摘要。

表 13-2:本章摘要

问题 解决方案 清单
创建一个新迁移 使用dotnet ef migration add命令 12
参阅迁移所包含的更改 使用dotnet ef migrations script command命令 3、10、13
对数据库应用迁移 使用dotnet ef database update command命令 4-9、15-17
列出项目中的迁移 使用dotnet ef migragions list命令 14
移除一个迁移 使用dotnet ef migrations remove命令 18-20
重置数据库 使用dotnet ef database update 0命令或dotnet ef database drop命令 21、22
管理多个数据库的迁移 创建单独的 context 类,并在使用dotnet ef命令时指定它的名称 23-28、30、31
在项目中查看数据库 context 使用dotnet ef dbcontext list命令 29
以编程方式管理迁移 使用 Entity Framework Core 迁移 API 32-35
以编程方式为数据库添加种子 使用迁移 API 确保在使用 context 类向数据库添加数据之前没有挂起的更新。 36-39

准备本章

本章使用第11章创建并于12章更改的 DataApp 项目。要准备本章,在 DataApp 项目文件夹中打开一个命令提示符或 PowerShell 窗口,并运行清单 13-1 所示的命令。

提示:如果您不想跟着构建示例项目,可以从本书的源代码存储库下载所有必需的文件,https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc

清单 13-1:重置数据库

dotnet ef database drop --force

此命令删除数据库,这将有助于确保从本章的示例中获得正确的预期结果。接下来,通过运行清单13-2所示的命令来启动应用程序。

清单 13-2:启动示例应用程序

dotnet run

应用程序启动,但当您使用浏览器请求 URL http://localhost:5000 时,将返回一个错误。堆栈跟踪很长,错误将重复多次,但以下是消息的重要部分:

SqlException: Cannot open database "DataAppDb" requested by the login.

理解迁移

上一节中报告错误的原因是应用程序的数据库并没准备好。appsettings.json 文件的连接字符串指定一个名为 DataAppDb 的数据库,如下所示:

"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=DataAppDb;MultipleActiveResultSets=true"

SQL Server目前对 DataAppDb 数据库一无所知,因此当应用程序试图从数据库读取数据时,产生了错误。数据库是使用 Entity Framework Core 和迁移来准备的。迁移是一个 C# 类,它包含创建数据架构的指令。当应用一个迁移时,将创建存储实体数据所需的数据库、表和列。

使用初始迁移

在示例应用程序中已经存在一个迁移,该迁移是在第 11 章中创建的,应用程序在前几章中使用了该迁移。打开 DataApp/Migrations 文件夹,您将看到三个文件。(其中一个文件嵌套在解决方案资源管理器中,在展开父项之前是不可见的。)所有三个文件在表 13-3 进行了描述。

表 13-3:示例项目中的迁移文件

名称 描述
<timestamp>_Initial.cs 它是Initial类的一个部分,它将第一次迁移应用于数据库,包含了创建数据库架构的指令。
<timestamp>_Initial.Designer.cs 它是Initial类的一个部分,它将第一次迁移应用于数据库,包含了创建模型对象的指令。
EFDatabaseContextModelSnapshot.cs 该类包含迁移中使用的实体类的描述,并用于检测创建进一步迁移的更改。

前两个文件是部分类,这意味着它们包含的代码被组合成一个 C# 类。它们的名字以时间戳开始,表明它们的创建时间,并遵循它们所包含的迁移的名称。此例名称包含_Initial,因为在 11 章所创建的迁移使用了此命令(您不需要再次运行此命令):

dotnet ef migrations add Initial

迁移约定是使用项目中创建的第一个迁移的名称Initial。打开<timestamp>_Initial.cs文件,您将看到迁移过程中最重要的部分,如下所示:

using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace DataApp.Migrations
{
    public partial class Initial : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Products",
                columns: table => new
                {
                    Id = table.Column<long>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    Name = table.Column<string>(nullable: true),
                    Category = table.Column<string>(nullable: true),
                    Price = table.Column<decimal>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Products", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Products");
        }
    }
}

注意:确切的代码可能会更改,特别是如果您正在使用 Entity Framework Core 的更高版本或 SQL 服务器的数据库提供程序。但是,代码的目的和一般性质将足够接近于理解迁移所做的事情。

译者注:本代码是在 Visual Studio 2019 中自动生成的代码。

Initial类的这一部分包含将被调用以更新数据库的方法。每个迁移包含两个方法,UpDown,如表13-4所述。

表 13-4:迁移方法

名称 描述
Up() 此方法包含更新数据库以存储实体数据的语句。
Down() 此方法包含将数据库降级到其原始状态的语句。

迁移可以用来升级或降级数据库,这个过程很快就会更有意义。对于新创建的数据库,升级过程将创建存储数据所需的表和列,而降级处理将使数据库返回到其原始状态。这两个过程在下面的章节中都有描述。


理解其它 EF 命令

使用 Entity Framework Core 意味着使用命令行创建和应用迁移。我在这本书中使用的所有命令都是以dotnet ef开头的,比如第11章中的这个命令:

dotnet ef migration add Initial

此命令创建一个新的迁移。它使用类似的命令应用于数据库,如本章稍后清单13-4所示:

dotnet ef database update

如果您使用的是早期版本的 Entity Framework,可能熟悉不同的命令集,例如Add-MigrationUpdate-Database。这些是用于管理迁移和数据库的原始命令,但它们仅被 Visual Studio 支持,并且只有在【程序包管理器控制台】中使用时才能可靠工作,这是一个带有一些附加功能的 PowerShell 窗口。

这种风格的命令仍然支持,但我在本书中没有使用。这些命令仅工作在特定的 Visual Studio 窗口,它们会引发无尽的问题。


理解迁移升级

Up方法负责数据库的升级,在处理应用于新创建的数据库的迁移时, Entity Framework Core 为 context 类的每个DbSet<T>属性创建一个表。目前 context 中只有一个DbSet属性,名为Products,它导致迁移中的这条语句:

...
migrationBuilder.CreateTable(
    name: "Products",
...

Up方法的参数是MigrationBuilder类的一个实例,它提供了方法以将指定的更改应用于数据库。这些方法由数据库提供程序转换为特定于数据库的命令,也就是迁移中的 C# 语句如何转换为 SQL Server 可以执行的 SQL 语句。

MigrationBuilder.CreateTable方法创建了一个新的数据表。默认情况下,实体框架核心使用DbSet<T>属性的名称作为数据表的名称,这就是为什么示例应用程序迁移中将CreateTablename参数被设置为Products的原因。

CreateTable方法的其他参数用于配置数据表。columns参数用于定义列,这些列是将用于存储数据的表的,由实体类定义的每个属性。由 context 类定义的Products属性的类型是DbSet<Product>,所以 Entity Framework Core 向表中为Product类所定义的每个属性添加列。

...
migrationBuilder.CreateTable(
    name: "Products",
    columns: table => new
    {
        Id = table.Column<long>(nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Name = table.Column<string>(nullable: true),
        Category = table.Column<string>(nullable: true),
        Price = table.Column<decimal>(nullable: false)
    },
...

列是是IdNameCategoryPrice属性定义的。Id属性配置为当新行添加进数据表时由数据库生成值。每个列都被配置为Product属性所对应的 .NET 类型。

为完成表的定义,添加了一个约束,将Id列指定为主键。

 migrationBuilder.CreateTable(
    name: "Products",
    columns: table => new
    {
        Id = table.Column<long>(nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Name = table.Column<string>(nullable: true),
        Category = table.Column<string>(nullable: true),
        Price = table.Column<decimal>(nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Products", x => x.Id);
    });

将这些代码放在一起,Up方法将创建一个名为Products的新表,其中配置了 Id、Name、Category 和 Price 列,Id 列是主键,并且在创建新行时,数据库将生成它的值。

理解迁移降级

Down方法用于将数据库返回之前的状态,取消Up方法的效果。在处理应用于空数据库的迁移时,降级只需删除为存储实体数据而创建的表。

...
migrationBuilder.DropTable(name: "Products");
...

MigrationBuilder.DropTable命令从数据库移除一张表。在本例中,Products 表将被移除,数据库将返回它的原始状态。

检查迁移 SQL

要查看 Initial 迁移类中的命令如何转换为 SQL 语句,请使用命令提示符在 DataApp 文件夹中运行清单 13-3 所示的命令。

清单 13-3:检查迁移

dotnet ef migrations script

所有的 Entity Framework Core 命令行工具都使用dotnet ef调用,所针对的是在第 11 章中添加到项目中的 EF Core 工具。migrations参数选择对迁移执行操作的工具,script参数显示迁移将在数据库中执行的 SQL 命令。对于示例项目中的 Initial 迁移,将生成以下 SQL 脚本:

...
IF OBJECT_ID(N'__EFMigrationsHistory') IS NULL
BEGIN
    CREATE TABLE [__EFMigrationsHistory] (
    [MigrationId] nvarchar(150) NOT NULL,
    [ProductVersion] nvarchar(32) NOT NULL,
    CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
    );
END;

GO

CREATE TABLE [Products] (
    [Id] bigint NOT NULL IDENTITY,
    [Category] nvarchar(max) NULL,
    [Name] nvarchar(max) NULL,
    [Price] decimal(18, 2) NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id])
);

GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20180124114307_Initial', N'2.0.1-rtm-125');

GO

这些语句创建了两张表。一个名为 Products,对应于上一节中描述的迁移中包含的指令。另一张表名为 __EFMigrationsHistory,它用于跟踪哪些迁移已经应用到数据库中,在本章后面创建其他迁移时,它的重要性将变得非常清楚。

应用迁移

在将迁移应用到数据库之前,迁移不会生效。在 DataApp 文件夹中运行清单 13-4 所示的命令,将迁移应用到数据库。

清单 13-4:应用迁移

dotnet ef database update

dotnet ef命令的目标是 Entity Framework Core 工具;database参数指定要对数据库执行操作,update参数告诉 Entity Framework Core 通过应用项目中的所有迁移来更新数据库(尽管目前只有一个迁移)。项目将被编译,Initial类的Up方法中的代码将被执行以生成创建和配置 Products 表的 SQL 命令。

提示:当您使用dotnet ef database update命令应用迁移时,如果连接字符串中指定的数据库还不存在,则将创建该数据库。应用程序配置使用的 DataAppDb 数据库就是这样形成的,尽管数据库的名称不是它生成的迁移的 SQL 脚本的一部分。

检查迁移

选择【工具】➤【SQL Server】➤【New Query】,在【服务器名称】栏中输入(localdb)\MSSQLLocalDB。确保【身份验证】栏中选择了【Windows 身份验证】,单击【数据库名称】菜单,下拉列表将显示使用 LocalDB 创建的数据库的完整列表,在其中选择【DataAppDB】。一旦连接到数据库,将清单 13-5 所示的 SQL 输入编辑器窗口。

USE DataAppDb

SELECT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Products'

GO

在 Visual Studio 的【SQL】菜单中选择【Execute】以向数据库服务器发送查询。返回的结果中包含了 Productrs 表中每一列的名称和类型,它显示了迁移的效果,如表13-5所示,并显示 Products 表中的一行如何包含表示Product对象所需的所有数据值的列。

表 13-5:Products 表的结构

列名 数据类型
Id bigint
Category nvarchar
Name nvarchar
Price decimal

播种数据库并运行应用程序

现在,数据库架构已经建立,Entity Framework Core 将可以代表示例应用程序读取并保存Product对象。

为向应用程序提供一些要处理的数据,请将清单 13-6 所示的 SQL 输入【SQL查询窗口】(或打开一个新的查询窗口)。

清单 13-6:播种数据库

USE DataAppDb

INSERT INTO Products (Name, Category, Price)
VALUES
	('Kayak', 'Watersports', 275),
	('Lifejacket', 'Watersports', 48.95),
	('Soccer Ball', 'Soccer', 19.50),
	('Corner Flags', 'Soccer', 34.95),
	('Stadium', 'Soccer', 79500),
	('Thinking Cap', 'Chess', 16),
	('Unsteady Chair', 'Chess', 29.95),
	('Human Chess Board', 'Chess', 75),
	('Bling-Bling King', 'Chess', 1200)

选择【SQL】➤【Execute】以播种数据库并接收到响应,该响应报告了命令所更改的行数:

(9 row(s) affected)

手工播种数据库是一个有点尴尬的过程,我会在本章稍后演示如何使用 C# 代码处理这些。在 DataApp 文件夹中运行清单 13-7 所示的命令,启动示例应用程序。

清单 13-7:启动示例应用程序

dotnet run

打开一个新的浏览器窗口,并请求 URL:http://localhost:5000。您将看到如图 13-1 所示的数据表。一旦确认数据库正常工作,并填充了种子数据,使用【Ctrl + C】停止应用程序。

图13-1 运行示例应用程序

在继续之前,值得考虑一下数据库迁移的效果。本节开始时没有数据库,也没有存储Product对象的方法。迁移是在第 11 章中创建的,它是一系列包含用于升级或降级数据库的指令的 C# 文件。在应用迁移时,将创建 DataAppDb 数据库,并将 C# 升级语句转换为 SQL 命令,执行这些命令以创建和配置 Products 表,其中包含Product类定义的每个属性的列。Products 表中的每一行表示一个Product对象,从清单 13-6 中创建的种子数据开始。

同样值得思考的是,这一过程在多大程度上是完全自动的。Entity Framework Core 发现了数据库 context 类,标识了需要存储在数据库中的类,并研究了如何将这些类的实例表示为关系数据。Entity Framework Core 可能需要为更复杂的示例提供一些方向,正如您将在后面的章节中看到的那样,但是,定义 C# 类有很长的路要走,可以让 Entity Framework Core 帮您解决细节问题。

创建附加迁移

当 MVC 应用程序中的数据模型发生变化时,迁移的便利性变得非常明显。要查看 Entity Framework Core 如何处理数据模型的更改,请编辑Product类以添加清单 13-8 所示的枚举和属性。

清单 13-8:Models 文件夹下的 Product.cs 文件,添加一个属性

namespace DataApp.Models
{
    public enum Colors
    {
        Red, Green, Blue
    }
    public class Product
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
        public Colors Color { get; set; }
    }
}

Color属性使用的是Colors枚举。添加属性意味着Product对象不能再存储在数据库中,因为没有存储Color值的途径。

使数据库与修订后的数据模型保持同步意味着创建和应用新的迁移。要创建迁移,请在 DataApp 文件夹中运行清单 13-9 所示的命令。

清单 13-9:创建一个新的数据库迁移

dotnet ef migrations add AddColorProperty

dotnet ef migrations add命令创建一个新的迁移,在本例中名为 AddColorProperty。您可以在迁移中使用任何您喜欢的名称,常见的命名策略包括描述迁移所反映的数据模型更改,或者使用增量版本号。

当您运行了清单 13-9 中的命令后,新文件被添加至 Migrations 文件夹。打开<timestamp>_AddColorProperty.cs文件查看迁移将应用到数据库中的更改,如下所示:

using Microsoft.EntityFrameworkCore.Migrations;

namespace DataApp.Migrations
{
    public partial class AddColorProperty : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<int>(
                name: "Color",
                table: "Products",
                nullable: false,
                defaultValue: 0);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropColumn(
                name: "Color",
                table: "Products");
        }
    }
}

Up方法将更新架构以反映数据模型中的更改,在本例中,这意味着向 Products 表中添加一个名称为 Color 的列。Down方法通过删除Color列将数据库返回到以前的状态。

在 DataApp 文件夹下运行清单 13-10 的命令,并查看新迁移所产生的 SQL 语句。

清单 13-10:检查迁移 SQL 语句

dotnet ef migrations script Initial AddColorProperty

在使用dotnet ef migrations script命令时指定迁移的名称将显示从第一次迁移到第二次迁移更新数据库所需的语句,并将产生以下结果:

ALTER TABLE [Products] ADD [Color] int NOT NULL DEFAULT 0;

GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190422134749_AddColorProperty', N'2.2.4-servicing-10062');

GO

这些语句将一个Color属性添加到 Products 表中,并更新 __EFMigrationsHistory 表以反映迁移到数据库的应用程序。

向数据模型中添加另一个属性

我准备创建第三个迁移以演示如何管理对数据库的更改。向Product类添加清单 13-11 所示的属性。

清单 13-11:Models 文件夹下的 Product.cs 文件,添加一个附加属性

namespace DataApp.Models
{
    public enum Colors
    {
        Red, Green, Blue
    }
    public class Product
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
        public Colors Color { get; set; }
        public bool InStock { get; set; }
    }
}

新添加的是一个名为InStockbool属性。要创建一个迁移以添加对在数据库中存储该属性的支持,在 DataApp 文件夹中运行清单 13-12 所示的命令。

清单 13-12:添加一个新迁移

dotnet ef migrations add AddInStockProperty

一系列新的迁移类文件将被添加至项目的 Migrations 文件夹。在 DataApp 文件夹运行清单 13-13 所示的命令,并查看更改 Products 表的 SQL 语句。

清单 13-13:显示迁移的 SQL 语句

dotnet ef migrations script AddColorProperty AddInStockProperty

这些命令将产生如下结果:

ALTER TABLE [Products] ADD [InStock] bit NOT NULL DEFAULT 0;

GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190423074949_AddInStockProperty', N'2.2.4-servicing-10062');

GO

您可以看到,对Product类的每一次更改都会产生迁移,从而使数据模型和数据库恢复同步。当创建迁移时,Entity Framework Core 将执行什么变化必须自动应用到数据库,以便迁移仅进行必要的修改表现修改类的实例。

管理迁移

创建迁移仅是进程的一部分:迁移还必须应用到数据库以确保应用程序数据被保存。管理迁移最常用的方法是使用dotnet ef命令行工具,我将在之后的章节描述它。还可以使用 Entity Framework Core 提供的 API 来管理迁移,我在本章的《以编程方式管理迁移》一节中对此进行了描述。

迁移列表

您可以通过在 DataApp 文件夹下运行清单13-14所示命令查看已经为示例项目创建的迁移列表。

清单 13-14:可用迁移列表

dotnet ef migrations list

此命令列出所有为项目创建的迁移。DataApp 项目中已经有三个迁移,类似以下输出(您在文件名中看到的将是时间戳):

...
<timestamp>_Initial
<timestamp>_AddColorProperty
<timestamp>_AddInStockProperty
...

这是在项目中定义的迁移列表,这些迁移可能尚未应用于数据库。迁移是按顺序列出的,这意味着您可以很容易地判断AddColorProperty迁移建立在Initial迁移中所包含的更改的基础上,AddInStockProperty迁移构建在AddColorProperty上。

应用所有迁移

最常见的任务是应用项目中定义的所有迁移,在一个步骤中更新数据库。要在 DataApp 项目中应用所有迁移,请在 DataApp 文件夹中运行清单 13-15 所示的命令。

清单 13-15:应用所有迁移

dotnet ef database update

Entity Framework Core 使用 __EFMigrationsHistory 来指示哪些迁移已经应用,并将使数据库更新。

选择【工具】➤【SQL Server】➤ 【New Query】,连接到数据库,并键如清单 13-16 所示的 SQL。

清单 13-16:检查 Products 表

USE DataAppDb

SELECT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Products'

GO

在 Visual Studio 的【SQL】菜单选择【Execute】以向数据库服务器发送查询,结果将提供如表 13-6 所示的表结构摘要。摘要包含了清单13-8以及清单13-11中添加到Product类的新属性所对应的列。

表 13-6:Products 表的结构

列名 数据类型
Id bigint
Category nvarchar
Name nvarchar
Price decimal
Color int
InStock bit

更新指定迁移

您可以将数据库更新为指定的迁移,当您需要回滚一组更改或不想一次将所有迁移应用到项目中时,这是非常有用的。在 DataApp 文件夹下运行清单 13-17 的命令以将数据库迁移至 AddColorProperty 迁移。

清单 13-17:更新数据到指定迁移

dotnet ef database update AddColorProperty

当迁移的名称与dotnet ef database update命令一起使用,Entity Framework Core 将检查数据库以查看哪些迁移已被应用,并开始目标迁移工作,调用UpDown方法与执行所需的升级或降级以达到目标状态。

对于清单 13-17 中的命令,要更新到 AddColorProperty 迁移,Entity Framework Core 必须从 AddInStockProperty 迁移中执行降级,这样做的效果是从 Products 表中删除 InStock 列。如果再次执行清单 13-16 中的 SQL 查询,您可以看到对表结构的更改,它显示了表 13-7 中的表结构。

表 13-7:Products 表结构

列名 数据类型
Id bigint
Category nvarchar
Name nvarchar
Price decimal
Color int

移除迁移

迁移有时会超出它的作用而造成错误。在 DataApp 文件夹中运行清单 13-18 所示的命令,以删除添加到项目中的最新迁移。

清单 13-18:移除最近的迁移

dotnet ef migrations remove

清单 13-18 的命令移除了 AddInStockProperty 迁移,这一点可以从产生的输出中看到。

...
Removing migration '<timestamp>_AddInStockProperty'.
Reverting model snapshot.
...

有关恢复模型快照的消息涉及到 Migrations 文件夹中的 EFDatabaseContextModelSnapshot.cs 文件,该文件用于将项目中的实体类与最近的迁移进行比较。删除迁移时,用于比较的快照将被更新以反映更改。如果要删除迁移,这非常有用,如,您拼错了名称,因为这意味着您创建的下一个迁移将反映自项目中最近的迁移以来模型类中的任何更改。

提示:您可以仅移除最近的迁移,这意味着如果要移除多个迁移,将不得不多次运行清单 13-18 的命令。

当要删除的迁移已经应用到数据库时,您将看到一个警告。这一点很重要,因为您可能会将数据库置于无法升级的状态,因为结构与其他迁移期望找到的状态不匹配。要查看警告,请在 DataApp 文件夹中运行清单 13-19 中的命令。

清单 13-19:移除已经被应用的迁移

dotnet ef migrations remove

此命令告诉 Entity Framework Core 移除已经被应用到数据库的 AddColorProperty 迁移。您将看到以下警告,并且迁移将不会被移除:

...
The migration '<timestamp>_AddColorProperty' has already been applied to the database.Revert it and try again. If the migration has been applied to other databases, consider reverting its changes using a new migration.
...

您可以使用dotnet ef database update命令从数据库中移除迁移,或如清单 13-20 所示,使用--force参数告诉 Entity Framework Core 继续命令,并从项目中移除迁移。在 DataApp 文件夹中运行此命令,强制移除迁移,即使它已应用于数据库。

清单 13-20:强制移除一个已经应用的迁移

dotnet ef migrations remove --force

当强制移除迁移时,Entity Framework Core 在从项目中删除迁移之前不会检查数据库的状态。这在命令的输出中得到确认,如下所示:

...
Removing migration '<timestamp>_AddColorProperty' without checking the database. If this migration has been applied to the database, you will need to manually reverse the changes it made.
Removing migration '<timestamp>_AddColorProperty'.
Reverting model snapshot.
...

重置数据库

有时,您希望撤消应用于数据库的所有迁移,然后重新开始。这可能是因为您希望测试迁移是否生成预期的结构,或者是因为您强制删除了应用于数据库和结构,并与项目不同步的迁移。

在 DataApp 文件夹中运行清单 13-21 所示的命令,以运行项目中所有迁移的Down方法中的命令。

清单 13-21:降级所有迁移

dotnet ef database update 0

dotnet ef database update命令中使用0参数将移除已经应用到数据库的所有迁移。这与返回原始起点不太一样,因为 DataAppDb 数据库和 __EFMigrationsHistory 表仍然存在(尽管该表是空的,因为没有应用迁移)。运行清单 13-22 所示的命令,返回完全干净的状态。

清单 13-22:删除数据库

dotnet ef database drop --force

此命令完全删除 DataAppDb 数据库,包括 __EFMigrationsHistory 表。如果您希望在删除数据库之前被提示,可以省略--force参数。

使用多个数据库

到目前为止,本章中的所有示例都假设项目只依赖于一个数据库。当使用dotnet ef migrationsdotnet ef database命令时,它们会检查项目以查找 context 类,使用关联的连接字符串连接到数据库,并完成他们的工作。

如果一个项目依赖多个数据库,这可能是因为有一个数据库用于产品数据,另一个数据库用于用户数据,则迁移操作所影响的 context 类必须指定为命令行的一部分。后面的部分向示例项目添加第二个数据库,并演示如何创建和应用其迁移。


决定何时创建不同的数据库

数据库可以包含多个表,每个表都可以存储不同类型的数据对象。对于许多项目来说,这意味着应用程序所需的所有数据都可以存储在一个数据库中,并通过一个 context 类访问。

使用多个数据库的最常见原因是当项目依赖于使用 Entity Framework Core(如 Asp.net Core Identity)的第三方包时,该包用于管理用户帐户、身份验证和授权。应用程序的自定义数据(相当于示例应用程序中的产品数据)存储在一个数据库中,身份数据存储在另一个数据库中。如果项目必须处理遗留应用程序中的数据,或者不同类型的数据具有不同的性能或安全性要求,则还可能需要多个数据库。


扩展数据模型

将第二个数据库添加到示例应用程序的起点是创建一个新的实体类、一个新的存储库及其实现类。首先,将一个名为 Customer.cs 的文件添加到 Models 文件夹中,并添加清单 13-23 所示的代码。

清单 13-23:Models 文件夹下的 Customer.cs 文件的内容

namespace DataApp.Models
{
    public class Customer
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public string Country { get; set; }
    }
}

Customer类定义一个Id属性,该属性将用作唯一键,其值将由数据库生成。NameCityCountry属性是常规字符串值。

若要创建用于处理Customer对象的 context 类,请在 Models 文件夹内新建名为 EFCustomerContext.cs 的类文件,并添加清单 13-24 所示的代码。

清单 13-24:Models 文件夹下的 EFCustomerContext.cs 文件的内容

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.DependencyInjection;

namespace DataApp.Models
{
    public class EFCustomerContext : DbContext
    {
        public EFCustomerContext(DbContextOptions<EFCustomerContext> opts)
            : base(opts) { }
        public DbSet<Customer> Customers { get; set; }
    }
}

EFCustomerContext类遵循 context 的标准模式,定义接收配置对象的构造函数和返回DbSet<T>的属性,其类型参数指示 context 管理的对象的类型。

要为Customer对象创建一个存储库,请向 Models 文件夹中添加一个名为 CustomerRepository.cs 的类文件,并添加清单 13-25 中所示的代码。为了简单起见,这个文件包含接口和实现类。

清单 13-25:Models 文件夹下的 CustomerRepository.cs 文件的内容

using System.Collections.Generic;

namespace DataApp.Models
{
    public interface ICustomerRepository
    {
        IEnumerable<Customer> GetAllCustomers();
    }

    public class EFCustomerRepository : ICustomerRepository
    {
        private EFCustomerContext context;
        public EFCustomerRepository(EFCustomerContext ctx)
        {
            context = ctx;
        }
        public IEnumerable<Customer> GetAllCustomers()
        {
            return context.Customers;
        }
    }
}

存储库提供了一个GetAllCustomers方法,该方法返回数据库中的所有Customer对象。我省略了第 12 章中描述的其他标准数据操作,以使示例尽可能简单。

配置应用程序

需要一个连接字符串,以便数据库提供程序能够到达数据库服务器,进行身份验证,并使用新的数据库。编辑 appsettings.json 文件并添加如清单 13-26 所示的连接字符串。

清单 13-26:DataApp 文件夹下的 appsettings.json 文件,定义连接字符串

{
  "Logging": {
    "LogLevel": {
      "Default": "None",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=DataAppDb;MultipleActiveResultSets=true",
    "CustomerConnection": "Server=(localdb)\\MSSQLLocalDB;Database=CustomerDb;MultipleActiveResultSets=true"
  }
}

名为CustomerConnection的新连接字符串在用于Product对象的同一 LocalDB 数据库服务器上指定一个名为 CustomerDb 的数据库。

提示:示例应用程序的数据库都由同一个数据库服务器管理,数据库服务器是通过 LocalDB 特性访问的 SQL 服务器。这不是必须的,您可以在应用程序中使用单独的数据库服务器,甚至可以混合和匹配来自不同供应商的数据库服务器,例如,应用程序可以同时使用 SQL Server 和 MySQL 数据库。

下一步是将 Entity Framework Core 配置为使用新数据库,并配置 ASP.NET Core 的依赖注入功能,以处理客户存储库接口上的依赖关系。编辑 Startup 类中的ConfigureServices方法,以添加清单 13-27 所示的语句。

清单 13-27:DataApp 文件夹下的 Startup.cs 文件,配置应用程序

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DataApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace DataApp
{
    public class Startup
    {
        public Startup(IConfiguration config) => Configuration = config;
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            string conString = Configuration["ConnectionStrings:DefaultConnection"];
            services.AddDbContext<EFDatabaseContext>(options =>
                options.UseSqlServer(conString));

            string customerConString =
                Configuration["ConnectionStrings:CustomerConnection"];
            services.AddDbContext<EFCustomerContext>(options =>
                options.UseSqlServer(customerConString));

            services.AddTransient<IDataRepository, EFDataRepository>();
            services.AddTransient<ICustomerRepository, EFCustomerRepository>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

创建并应用迁移

现在应用程序中已经有了两个 context 类,本章前面使用的命令将无法工作。要查看此问题,请在 DataApp 文件夹中运行清单 13-28 所示的命令。

清单 13-28:创建迁移

dotnet ef migrations add Customers_Initial

此命令显示如下错误信息:

More than one DbContext was found. Specify which one to use. Use the '-Context' parameter for PowerShell commands and the '--context' parameter for dotnet commands.

当 Entity Framework Core 检查项目时,它会发现两个从 DbContext 派生的类,并且不知道应该将迁移命令应用到哪个类。

您不需要搜索项目就可以找到已经发现的 context 类。相反,在 DataApp 文件夹中运行清单 13-29 所示的命令,Entity Framework Core 将显示它找到的 context 类的列表。

清单 13-29:列出 Context 类

dotnet ef dbcontext list

此命令显示项目中 context 类的列表,如下所示:

DataApp.Models.EFDatabaseContext
DataApp.Models.EFCustomerContext

类的名称(可带或不带命名空间)与--context参数一起使用即可选择一个 context 进行操作。在 DataApp 文件夹下运行清单 13-30 所示的命令以为Customer类创建初始迁移,并且为了反映在本章前面删除迁移时创建的Product类中未捕获的更改,这一次指定了它应用到的 context。

清单 13-30:在创建迁移时指定 Context

dotnet ef migrations add Initial --context EFCustomerContext
dotnet ef migrations add Current --context EFDatabaseContext

将迁移应用到数据库需要同样的技术。在 DataApp 文件夹中运行清单 13-31 所示的命令,为两个数据库应用迁移。

清单 13-31:为数据库应用迁移

dotnet ef database update --context EFDatabaseContext
dotnet ef database update --context EFCustomerContext

以编程方式管理迁移

对于大多数项目,管理迁移的最佳方法是使用dotnet ef migrationsdotnet ef database命令行工具。但是,Entity Framework Core 还提供了一个 API,用于以编程方式管理迁移,当无法使用命令行时,该 API 可能非常有用。迁移 API 可用于创建迁移管理工具,该工具可用于决定将哪些迁移应用于数据库。在本节中,我将演示如何创建迁移管理器并演示迁移 API 如何工作。

警告:不要使用 ASP.NET Core Identity 来管理对迁移管理工具的访问。Identity 依赖于 Entity Framework Core 的数据存储,这意味着您可以轻松地应用迁移,从而阻止您验证自己,从而阻止您使用管理工具。我通常将迁移管理工具单独安装在数据库服务器旁边,以使其与面向公共的 MVC 应用程序保持分离。

创建迁移管理器类

创建迁移管理器的起点是定义一个助手,它将以一种易于从 MVC 控制器使用的方式处理 Entity Framework Core API。在 Models 文件夹中创建一个名为 MigrationsManager.cs 的类文件,并添加清单 13-32 所示的代码。

清单 13-32:Models 文件夹下的 MigrationsManager.cs 文件的内容

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace DataApp.Models
{
    public class MigrationsManager
    {
        private IEnumerable<Type> ContextTypes;
        private IServiceProvider provider;
        public IEnumerable<string> ContextNames;

        public MigrationsManager(IServiceProvider prov)
        {
            provider = prov;
            ContextTypes = provider.GetServices<DbContextOptions>()
            .Select(o => o.ContextType);
            ContextNames = ContextTypes.Select(t => t.FullName);
            ContextName = ContextNames.First();
        }

        public string ContextName { get; set; }

        public IEnumerable<string> AppliedMigrations
            => Context.Database.GetAppliedMigrations();

        public IEnumerable<string> PendingMigrations
            => Context.Database.GetPendingMigrations();

        public IEnumerable<string> AllMigrations
            => Context.Database.GetMigrations();

        public void Migrate(string contextName, string target = null)
        {
            Context.GetService<IMigrator>().Migrate(target);
        }

        public DbContext Context =>
            provider.GetRequiredService(Type.GetType(ContextName)) as DbContext;
    }
}

此类有三个角色。第一个是获取应用程序中的数据库 context 类的类型和名称。管理器类获取使用 ASP.NET Core 依赖注入功能注册为服务的所有 DbContextOptions 服务,然后使用这些服务通过读取每个 DbContextOptions 对象的 ContextType 属性获取关联的 context 类,如下所示:

...
ContextTypes = provider.GetServices<DbContextOptions>().Select(o => o.ContextType);
...

这是一种有点尴尬的技术,但它与 Entity Framework Core 用来发现 context 类的技术是一样的。

管理器类的第二个角色是接受包含 context 类型名称的字符串,并使用 ASP.NET Core 依赖注入功能获取该类的实例,如下所示:

...
public DbContext Context =>
provider.GetRequiredService(Type.GetType(ContextName)) as DbContext;
...

这是到应用程序的 MVC 部分的桥梁,它将接收应该将迁移作为 HTML 表单中的字符串应用到的 context 的名称,并使用它来设置 ContextName 属性的值,该属性用于标识用户希望使用的 context。

第三个也是最后一个角色是在数据库 context 中执行迁移对象。Microsoft.EntityFrameworkCore命名空间包含一些扩展方法,这些扩展方法通过DbContext.Database属性返回的DatabaseFacade对象进行迁移,如表 13-8 所述。

表 13-8:迁移的 DatabaseFacade 扩展方法

名称 描述
GetMigrations() 此方法返回一系列表示迁移名称的字符串值。
GetAppliedMigrations() 此方法返回一系列被应用到数据库的迁移名称的字符串值。
GetPendingMigrations() 此方法返回一系列还未应用到数据库的迁移名称的字符串值。
Migrate() 此方法将所有挂起的迁移应用到数据库。

Migrate方法不允许指定特定的迁移。提供此功能意味着使用IMigrator服务,它在Microsoft.EntityFrameworkCore.Migrations命名空间中定义,此接口定义了一个不允许指定迁移的Migrate方法,以下就是如何实现MigrationsManager类的Migrate方法:

...
Context.GetService<IMigrator>().Migrate(target);
...

通过使用表 13-8 中描述的方法和IMigrator服务,管理器类能够提供关于每个数据库 context 类的迁移状态的信息,并应用和删除单个迁移。

创建迁移控制器和视图

接下来是创建一个控制器和视图,它将提供对MigrationsManager类的功能的访问。在 Controllers 文件夹下新建一个名为 MigrationsController.cs 的类文件,并添加清单 13-33 中的代码。

清单 13-33:Controllers 文件夹下的 MigrationsController.cs 文件的内容

using DataApp.Models;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace DataApp.Controllers
{
    public class MigrationsController : Controller
    {
        private MigrationsManager manager;

        public MigrationsController(MigrationsManager mgr)
        {
            manager = mgr;
        }

        public IActionResult Index(string context)
        {
            ViewBag.Context = manager.ContextName = context
            ?? manager.ContextNames.First();
            return View(manager);
        }

        [HttpPost]
        public IActionResult Migrate(string context, string migration)
        {
            manager.ContextName = context;
            manager.Migrate(context, migration);
            return RedirectToAction(nameof(Index), new { context = context });
        }
    }
}

Index action 方法用于显示项目中的 context 类和迁移的详细信息。POST-only Migrate方法用于将迁移应用到数据库。为给控制器提供所对应的视图,请创建 Views/Migrations 文件夹,并向其中添加一个名为 Index.cshtml 的 Razor 文件,并向其添加如清单 13-34 所示标记。

清单 13-34:Views/Migrations 文件夹下的 Index.cshtml 文件的内容

@using DataApp.Models
@model MigrationsManager
@{
    ViewData["Title"] = "Migrations";
    Layout = "_Layout";
}

<div class="m-1 p-2">
    <form asp-action="Index" method="get" class="form-inline">
        <label class="m-1">Database Context:</label>
        <select name="context" class="form-control">
            @foreach (var name in Model.ContextNames)
            {
                <option selected="@(name == ViewBag.Context)">@name</option>
            }
        </select>
        <button class="btn btn-primary m-1">Select</button>
    </form>
</div>

<table class="table table-sm table-striped m-2">
    <thead>
        <tr><th>Migration Name</th><th>Status</th></tr>
    </thead>
    <tbody>
        @foreach (string m in Model.AllMigrations)
        {
            <tr>
                <td>@m</td>
                <td>
                    @(Model.AppliedMigrations.Contains(m)
                    ? "Applied" : "Pending")
                </td>
            </tr>
        }
    </tbody>
</table>

<div class="m-1 p-2">
    <form asp-action="Migrate" method="post" class="form-inline">
        <input type="hidden" name="context" value="@ViewBag.Context" />
        <label class="m-1">Migration:</label>
        <select name="migration" class="form-control">
            <option selected value="@Model.AllMigrations.Last()">All</option>
            @foreach (var m in Model.AllMigrations.Reverse())
            {
                <option>@m</option>
            }
            <option value="0">None</option>
        </select>
        <button class="btn btn-primary m-1">Migrate</button>
    </form>
</div>

该视图包含用于选择所管理的 context 类的表单、列出迁移及其状态的表以及用于应用迁移的第二个表单。

配置应用程序

MigrationsManager类必须在 ASP.NET Core 依赖注入系统中注册,以便可以使用用于获取 context 对象的IServiceProvider对象创建新实例。编辑Startup类的ConfigureServices方法,添加如清单 13-35 所示的语句。

清单 13-35:DataApp 文件夹下的 Startup.cs 文件,注册助手类

...
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    string conString = Configuration["ConnectionStrings:DefaultConnection"];
    services.AddDbContext<EFDatabaseContext>(options =>
        options.UseSqlServer(conString));

    string customerConString =
        Configuration["ConnectionStrings:CustomerConnection"];
    services.AddDbContext<EFCustomerContext>(options =>
        options.UseSqlServer(customerConString));

    services.AddTransient<IDataRepository, EFDataRepository>();
    services.AddTransient<ICustomerRepository, EFCustomerRepository>();
    services.AddTransient<MigrationsManager>();
}
...

MigrationsManager类使用AddTransient注册,这意味着类上的每个依赖项都将使用一个新对象进行解析。

运行迁移管理器

使用dotnet run命令重启应用程序并导航至 URL:http://localhost:5000/migrations。通过使用窗口顶部的 select 元素选择一个项目,并单击【Select】按钮,您可以在上下文类之间切换。要升级或降级数据库,选择一个您想要的迁移并单击【Migrate】按钮,如图 13-2 所示。检查应用程序的输出,您将看到发送给数据库服务器的以在迁移间移动的 SQL 语句。

图13-2 以编程方式管理迁移

以编程方式播种数据库

许多数据库需要一些种子数据,以便应用程序可以使用一些基线数据。产品数据库通常是这样的,其中有一组初步的产品正在出售给客户,而安全数据库中至少有一个帐户,以便管理员可以登录并执行初始配置任务。

到目前为止,在这本书的这一部分,数据库已经被种子使用原始 SQL 直接发送到数据库服务器。这是一种向数据库添加数据的容易出错的方法,并且不使用 Entity Framework Core。

Entity Framework Core 不包括任何专门用于处理种子数据的内置支持,尽管在撰写本报告时,它已在已发布的路线图中。在有专门的种子特性之前,您可以使用两种技术来进行播种,这些技术将在下面的部分中描述。正如我所解释的,每种方法都有其优点和缺点,但总体效果是填充数据库,这样您就不必使用原始 SQL。

这两种方法都依赖于创建对象并使用 Entity Framework Core 将它们存储在数据库中。为了避免重复每种方法的代码,我在 Models 文件夹中创建了一个名为 SeedData.cs 的类文件,并添加了清单 13-36 所示的代码。

警告:清单 13-36 中的代码包含一个从数据库中删除数据的方法。这在 Entity Framework Core 的实验中很有用,因为您可以轻松地重置数据库并尝试不同的种子技术。对于实际项目,需要谨慎避免删除实际生产数据,并且您可能希望通过不在您自己的项目中实现等效的ClearData方法来降低风险。

清单 13-36:Models 文件夹下的 SeedData.cs 文件的内容

using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace DataApp.Models
{
    public static class SeedData
    {
        public static void Seed(DbContext context)
        {
            if (context.Database.GetPendingMigrations().Count() == 0)
            {
                if (context is EFDatabaseContext prodCtx
                    && prodCtx.Products.Count() == 0)
                {
                    prodCtx.Products.AddRange(Products);
                }
                else if (context is EFCustomerContext custCtx
                    && custCtx.Customers.Count() == 0)
                {
                    custCtx.Customers.AddRange(Customers);
                }
                context.SaveChanges();
            }
        }

        public static void ClearData(DbContext context)
        {
            if (context is EFDatabaseContext prodCtx
                && prodCtx.Products.Count() > 0)
            {
                prodCtx.Products.RemoveRange(prodCtx.Products);
            }
            else if (context is EFCustomerContext custCtx
                && custCtx.Customers.Count() > 0)
            {
                custCtx.Customers.RemoveRange(custCtx.Customers);
            }
            context.SaveChanges();
        }

        private static Product[] Products = 
        {
            new Product { Name = "Kayak", Category = "Watersports",
                Price = 275, Color = Colors.Green, InStock = true },
            new Product { Name = "Lifejacket", Category = "Watersports",
                Price = 48.95m, Color = Colors.Red, InStock = true },
            new Product { Name = "Soccer Ball", Category = "Soccer",
                Price = 19.50m, Color = Colors.Blue, InStock = true },
            new Product { Name = "Corner Flags", Category = "Soccer",
                Price = 34.95m, Color = Colors.Green, InStock = true },
            new Product { Name = "Stadium", Category = "Soccer",
                Price = 79500, Color = Colors.Red, InStock = true },
            new Product { Name = "Thinking Cap", Category = "Chess",
                Price = 16, Color = Colors.Blue, InStock = true },
            new Product { Name = "Unsteady Chair", Category = "Chess",
                Price = 29.95m, Color = Colors.Green, InStock = true },
            new Product { Name = "Human Chess Board", Category = "Chess",
                Price = 75, Color = Colors.Red, InStock = true },
            new Product { Name = "Bling-Bling King", Category = "Chess",
                Price = 1200, Color = Colors.Blue, InStock = true }
        };

        private static Customer[] Customers = 
        {
            new Customer { Name = "Alice Smith",
                City = "New York", Country = "USA" },
            new Customer { Name = "Bob Jones",
                City = "Paris", Country = "France" },
            new Customer { Name = "Charlie Davies",
                City = "London", Country = "UK" }
        };
    }
}

SeedData类定义了用于播种的ProductCustomer对象的静态属性,并定义了使用静态属性填充和清除数据库的方法。每个方法接收一个数据库 context 对象,标识 context 的类型,然后添加或删除数据。

Seed方法负责将种子数据添加到数据库中,首先检查是否有未完成的迁移,如下所示:

...
if (context.Database.GetPendingMigrations().Count() == 0) {
...

以编程方式播种数据库可能很棘手,因为用于填充数据库的对象必须与 Entity Framework Core 用来存储数据库表的结构相匹配。如果存在不匹配,那么 Entity Framework Core 将无法存储数据,播种将失败。在尝试存储种子数据之前检查没有挂起的迁移有助于降低不匹配的风险,但在尝试种子数据库之前,您仍然必须确保数据模型类的所有更改都已在迁移中捕获。

创建播种工具

第一种方法是创建数据库播种工具,类似于我在本章前面为管理迁移创建的工具。实际上,为了避免重复代码,我将扩展迁移工具,以便它也可以为数据库添加种子。

这是在生产系统中播种数据库的最佳方法。缺点是播种需要开发人员或管理员显式执行以将种子数据添加到数据库中,这在开发过程中可能会令人沮丧(《在启动期间播种》一节中所描述的技术更适合于此)。

第一步是向Migrations控制器添加新的 action 方法,该方法可用于播种和清除数据库,如清单 13-37 所示。

清单 13-37:Controllers 文件夹下的 MigrationsController.cs 文件,添加 Action

using DataApp.Models;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace DataApp.Controllers
{
    public class MigrationsController : Controller
    {
        private MigrationsManager manager;

        public MigrationsController(MigrationsManager mgr)
        {
            manager = mgr;
        }

        public IActionResult Index(string context)
        {
            ViewBag.Context = manager.ContextName = context
            ?? manager.ContextNames.First();
            return View(manager);
        }

        [HttpPost]
        public IActionResult Migrate(string context, string migration)
        {
            manager.ContextName = context;
            manager.Migrate(context, migration);
            return RedirectToAction(nameof(Index), new { context = context });
        }

        [HttpPost]
        public IActionResult Seed(string context)
        {
            manager.ContextName = context;
            SeedData.Seed(manager.Context);
            return RedirectToAction(nameof(Index), new { context = context });
        }

        [HttpPost]
        public IActionResult Clear(string context)
        {
            manager.ContextName = context;
            SeedData.ClearData(manager.Context);
            return RedirectToAction(nameof(Index), new { context = context });
        }
    }
}

新的 action 方法名为SeedClear,仅用于 POST 请求。每个方法都接收数据库 context 类的名称作为其参数,该参数与清单 13-32 中创建的MigrationsManager类一起使用,以获得可以传递给由种子数据类定义的方法之一的 context 对象。

在 action 方法就位之后,可以创建播种和清除数据库的按钮,如清单 13-38 所示。

清单 13-38:Views/Migrations 文件夹下的 Index.cshtml 文件,添加按钮

@using DataApp.Models
@model MigrationsManager
@{
    ViewData["Title"] = "Migrations";
    Layout = "_Layout";
}

<!-- ...代码省略... -->

<div class="m-1 p-2">
    <form method="post">
        <input type="hidden" name="context" value="@ViewBag.Context" />
        <button class="btn btn-primary" asp-action="Seed">Seed Database</button>
        <button class="btn btn-danger" asp-action="Clear">Clear Database</button>
    </form>
</div>

新元素定义了一个表单,每个 button 元素通过asp-action标签助手特性指定 action。要查看结果,使用dotnet run启动应用程序,导航至 http://localhost:5000/migrations,单击 Seed 或 Clear 按钮,如图 13-3 所示。

图13-3 给迁移工具添加播种支持

在启动期间播种

给数据库播种的另一个方法是在应用程序启动期间执行此操作。只有在开发过程中才应该这样做,特别是在生产中运行多个应用程序实例的情况下;否则,应用程序的多个实例可能尝试同时启动数据库,从而导致问题并阻止干净的启动。

这种方法的优点是它是自动的。如果数据库没有挂起的迁移,并且是空的,那么数据库将被播种。通过将清单 13-39 所示的语句添加到Startup类中,数据库播种可以作为应用程序启动的一部分。

清单 13-39:DataApp 文件夹下的 Startup.cs 文件,播种数据库

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using DataApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace DataApp
{
    public class Startup
    {
        public Startup(IConfiguration config) => Configuration = config;
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            string conString = Configuration["ConnectionStrings:DefaultConnection"];
            services.AddDbContext<EFDatabaseContext>(options =>
                options.UseSqlServer(conString));

            string customerConString =
                Configuration["ConnectionStrings:CustomerConnection"];
            services.AddDbContext<EFCustomerContext>(options =>
                options.UseSqlServer(customerConString));

            services.AddTransient<IDataRepository, EFDataRepository>();
            services.AddTransient<ICustomerRepository, EFCustomerRepository>();
            services.AddTransient<MigrationsManager>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
            EFDatabaseContext prodCtx, EFCustomerContext custCtx)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();

            if (env.IsDevelopment())
            {
                SeedData.Seed(prodCtx);
                SeedData.Seed(custCtx);
            }
        }
    }
}

Configure方法中声明参数使得我们可以从数据库接收 context 对象,并将其传递给SeedData.Seed方法。为了防止在生产中自动播种数据库,我检查环境以确保应用程序正在开发中运行。结果是,数据库在应用程序启动时自动播种,您可以通过使用上一节中创建的工具清除数据库,然后重新启动应用程序来查看数据库。


避免使用命令行工具出现问题

对于 Entity Framework Core 2,微软改变了数据库 context 的发现方式,这意味着即使命令行工具用于管理迁移或将其应用于数据库,也将执行清单 13-39 中添加的代码。这意味着有可能陷入播种代码试图用数据库无法存储或不存在的数据填充数据库的情况。如果发生这种情况,则在运行dotnet ef命令时将看到异常。要解决这个问题,请注释清单 13-39 中添加的对SeedData.Seed方法的调用,直到您完成迁移操作,此时您可以再次取消对它们的注释。


总结

在本章中,我解释了如何使用迁移来保持数据库模式与应用程序保持一致,并准备数据库以便存储应用程序的数据。我演示了创建新迁移的过程,向您展示了如何升级和降级数据库,并解释了如何使用 Entity Framework Core API 应用和管理迁移。在结束本章时,我向您展示了如何为生产和开发环境创建数据库。在下一章中,我将解释如何在对象之间创建关系并在数据库中表示它们。

;

© 2018 - IOT小分队文章发布系统 v0.3