第 4 部分,ASP.NET Core 中的 Razor Pages 和 EF Core 迁移

作者:Tom DykstraJon P SmithRick Anderson

Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor Pages Web 应用。 若要了解系列教程,请参阅第一个教程

如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。

本教程介绍用于管理数据模型更改的 EF Core 迁移功能。

开发新应用时,数据模型会频繁更改。 每当模型发生更改时,都无法与数据库进行同步。 本教程从配置实体框架以创建数据库(如果不存在)开始。 数据模型每次发生更改时,必须删除该数据库。 下次应用运行时,对 EnsureCreated 的调用将重新创建数据库以匹配新的数据模型。 然后 DbInitializer 类将运行以设定新数据库的种子。

这种使 DB 与数据模型保持同步的方法非常适用,但需要将应用部署到生产环境的情况除外。 当应用在生产环境中运行时,应用通常会存储需要保留的数据。 每当发生更改(例如添加新列)时,应用都无法在具有测试 DB 的环境下启动。 EF Core 迁移功能可通过使 EF Core 更新 DB 架构而不是创建新数据库来解决此问题。

数据模型更改时,迁移不会删除并重新创建数据库,而是更新架构并保留现有数据。

注意

SQLite 限制

本教程使用 Entity Framework Core 迁移功能(若可行)。 迁移会更新数据库架构,使其与数据模型中的更改相匹配。 但迁移只执行数据库引擎支持的更改类型,而 SQLite 的架构更改功能受限。 例如,支持添加列,但不支持删除列。 如果已创建迁移以删除列,则 ef migrations add 命令将成功,但 ef database update 命令会失败。

要绕开 SQLite 限制,可手动写入迁移代码,在表内容更改时重新生成表。 代码将在 UpDown 方法中用于迁移,并且将涉及以下内容:

  • 创建新表。
  • 将旧表中的数据复制到新表中。
  • 放弃旧表。
  • 为新表重命名。

本教程不涉及编写此类型的特定于数据库的代码。 相反,每当尝试应用迁移失败时,本教程将删除并重新创建数据库。 有关更多信息,请参见以下资源:

删除数据库

使用 SQL Server 对象资源管理器 (SSOX) 删除数据库或在包管理器控制台 (PMC) 中运行以下命令 :

Drop-Database

创建初始迁移

在 PMC 中运行以下命令:

Add-Migration InitialCreate
Update-Database

删除 EnsureCreated

本系列教程从使用 EnsureCreated 开始。 EnsureCreated 不创建迁移历史记录表,因此不能与迁移一起使用。 它专门用于在频繁删除并重新创建 DB 的情况下进行测试或快速制作原型。

从这个角度来看,教程将使用迁移。

Program.cs 中,删除以下行:

context.Database.EnsureCreated();

运行应用并验证数据库是否已设定种子。

Up 和 Down 方法

EF Coremigrations add 命令已生成用于创建数据库的代码。 此迁移代码位于 Migrations\<timestamp>_InitialCreate.cs 文件中。 InitialCreate 类的 Up 方法创建与数据模型实体集对应的数据库表。 Down 方法删除这些表,如下例所示:

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

namespace ContosoUniversity.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Course",
                columns: table => new
                {
                    CourseID = table.Column<int>(nullable: false),
                    Title = table.Column<string>(nullable: true),
                    Credits = table.Column<int>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Course", x => x.CourseID);
                });

            migrationBuilder.CreateTable(
                name: "Student",
                columns: table => new
                {
                    ID = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    LastName = table.Column<string>(nullable: true),
                    FirstMidName = table.Column<string>(nullable: true),
                    EnrollmentDate = table.Column<DateTime>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Student", x => x.ID);
                });

            migrationBuilder.CreateTable(
                name: "Enrollment",
                columns: table => new
                {
                    EnrollmentID = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    CourseID = table.Column<int>(nullable: false),
                    StudentID = table.Column<int>(nullable: false),
                    Grade = table.Column<int>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Enrollment", x => x.EnrollmentID);
                    table.ForeignKey(
                        name: "FK_Enrollment_Course_CourseID",
                        column: x => x.CourseID,
                        principalTable: "Course",
                        principalColumn: "CourseID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_Enrollment_Student_StudentID",
                        column: x => x.StudentID,
                        principalTable: "Student",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Enrollment_CourseID",
                table: "Enrollment",
                column: "CourseID");

            migrationBuilder.CreateIndex(
                name: "IX_Enrollment_StudentID",
                table: "Enrollment",
                column: "StudentID");
        }

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

            migrationBuilder.DropTable(
                name: "Course");

            migrationBuilder.DropTable(
                name: "Student");
        }
    }
}

前面的代码适用于初始迁移。 代码:

  • migrations add InitialCreate 命令生成。
  • database update 命令执行。
  • 为数据库上下文类指定的数据模型创建数据库。

迁移名称参数(本示例中为 InitialCreate)用于指定文件名。 迁移名称可以是任何有效的文件名。 最好选择能概括迁移中所执行操作的字词或短语。 例如,添加了系表的迁移可称为“AddDepartmentTable”。

迁移历史记录表

  • 使用 SSOX 或 SQLite 工具检查数据库。
  • 请注意,增加了 __EFMigrationsHistory 表。 __EFMigrationsHistory 表跟踪已应用到数据库的迁移。
  • 查看 __EFMigrationsHistory 表中的数据。 它显示第一次迁移的行。

数据模型快照

迁移会在 Migrations/SchoolContextModelSnapshot.cs 中创建当前数据模型的快照。 添加迁移时,EF 会通过将当前数据模型与快照文件进行对比来确定已更改的内容。

由于快照文件跟踪数据模型的状态,因此不能通过删除 <timestamp>_<migrationname>.cs 文件来删除迁移。 若要返回最近的迁移,必须使用 migrations remove 命令。 migrations remove 删除迁移,并确保正确重置快照。 有关详细信息,请参阅 dotnet ef migrations remove

若要删除所有迁移,请参阅重置所有迁移

在生产环境中应用迁移

不建议生产应用在应用程序启动时调用 Database.MigrateMigrate 不应从部署到服务器场的应用中调用。 如果应用横向扩展到多个服务器实例,则很难确保多个服务器不会发生数据库架构更新,或者这些更新不会与读/写访问冲突。

应在部署过程中以受控的方式执行数据库迁移。 生产数据库迁移方法包括:

  • 使用迁移创建 SQL 脚本,并在部署过程中使用 SQL 脚本。
  • 在受控的环境中运行 dotnet ef database update

疑难解答

如果应用使用 SQL Server LocalDB 并显示以下异常:

SqlException: Cannot open database "ContosoUniversity" requested by the login.
The login failed.
Login failed for user 'user name'.

解决方案可能是在命令提示符下运行 dotnet ef database update

其他资源

后续步骤

下一个教程将生成数据模型,并添加实体属性和新实体。

本教程使用 EF Core 迁移功能来管理数据模型更改。

如果遇到无法解决的问题,请下载已完成应用

开发新应用时,数据模型会频繁更改。 每当模型发生更改时,都无法与数据库进行同步。 本教程首先配置 Entity Framework 以创建数据库(如果不存在)。 每当数据模型发生更改时:

  • DB 都会被删除。
  • EF 都会创建一个新数据库来匹配该模型。
  • 应用使用测试数据为 DB 设定种子。

这种使 DB 与数据模型保持同步的方法非常适用,但需要将应用部署到生产环境的情况除外。 当应用在生产环境中运行时,应用通常会存储需要保留的数据。 每当发生更改(例如添加新列)时,应用都无法在具有测试 DB 的环境下启动。 EF Core 迁移功能可通过使 EF Core 更新 DB 架构而不是创建新 DB 来解决此问题。

数据模型发生更改时,迁移将更新架构并保留现有数据,而无需删除或重新创建 DB。

删除数据库

使用 SQL Server 对象资源管理器 (SSOX) 或 database drop 命令:

在“包管理器控制台”(PMC) 中运行以下命令:

Drop-Database

从 PMC 运行 Get-Help about_EntityFrameworkCore,获取帮助信息。

创建初始迁移并更新 DB

生成项目并创建第一个迁移。

Add-Migration InitialCreate
Update-Database

了解 Up 和 Down 方法

EF Coremigrations add 命令已生成用于创建数据库的代码。 此迁移代码位于 Migrations\<timestamp>_InitialCreate.cs 文件中。 InitialCreate 类的 Up 方法创建与数据模型实体集对应的数据库表。 Down 方法删除这些表,如下例所示:

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Course",
            columns: table => new
            {
                CourseID = table.Column<int>(nullable: false),
                Title = table.Column<string>(nullable: true),
                Credits = table.Column<int>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Course", x => x.CourseID);
            });

        migrationBuilder.CreateTable(
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Enrollment");

        migrationBuilder.DropTable(
            name: "Course");

        migrationBuilder.DropTable(
            name: "Student");
    }
}

迁移调用 Up 方法为迁移实现数据模型更改。 输入用于回退更新的命令时,迁移调用 Down 方法。

前面的代码适用于初始迁移。 该代码是运行 migrations add InitialCreate 命令时创建的。 迁移名称参数(本示例中为“InitialCreate”)用于指定文件名。 迁移名称可以是任何有效的文件名。 最好选择能概括迁移中所执行操作的字词或短语。 例如,添加了系表的迁移可称为“AddDepartmentTable”。

如果创建了初始迁移并且存在 DB:

  • 会生成 DB 创建代码。
  • DB 创建代码不需要运行,因为 DB 已与数据模型相匹配。 即使 DB 创建代码运行也不会做出任何更改,因为 DB 已与数据模型相匹配。

如果将应用部署到新环境,则必须运行 DB 创建代码才能创建 DB。

先前删除了 DB,因此已不存在,所以迁移会创建新的 DB。

数据模型快照

迁移在 Migrations/SchoolContextModelSnapshot.cs 中创建当前数据库架构的快照。 添加迁移时,EF 会通过将数据模型与快照文件进行对比来确定已更改的内容。

若要删除迁移,请使用以下命令:

Remove-Migration

删除迁移命令会删除迁移并确保正确重置快照。

删除 EnsureCreated 并测试应用

早期开发使用了 EnsureCreated。 本教程将使用迁移。 EnsureCreated 具有以下限制:

  • 绕过迁移并创建 DB 和架构。
  • 不会创建迁移表。
  • 不能与迁移一起使用。
  • 专门用于在频繁删除并重新创建 DB 的情况下进行测试或快速制作原型。

删除 EnsureCreated

context.Database.EnsureCreated();

运行应用并验证 DB 设定为种子。

检查数据库

使用 SQL Server 对象资源管理器检查 DB。 请注意,增加了 __EFMigrationsHistory 表。 __EFMigrationsHistory 表跟踪已应用到 DB 的迁移。 查看 __EFMigrationsHistory 表中的数据,其中显示对应初始迁移的一行数据。 上面的 CLI 输出示例中最后部分的日志显示了创建此行的 INSERT 语句。

运行应用并验证一切正常运行。

在生产环境中应用迁移

不建议生产应用在应用程序启动时调用 Database.Migrate。 不应从服务器场中的应用调用 Migrate。 例如,已将应用在云中部署为横向扩展(运行应用的多个示例)的情况。

应在部署过程中以受控的方式执行数据库迁移。 生产数据库迁移方法包括:

  • 使用迁移创建 SQL 脚本,并在部署过程中使用 SQL 脚本。
  • 在受控的环境中运行 dotnet ef database update

EF Core 使用 __MigrationsHistory 表查看是否需要运行任何迁移。 如果 DB 已是最新,则无需运行迁移。

疑难解答

下载已完成应用

应用会生成以下异常:

SqlException: Cannot open database "ContosoUniversity" requested by the login.
The login failed.
Login failed for user 'user name'.

解决方案:运行 dotnet ef database update

其他资源