第 4 部分:Razor 在 ASP.NET Core 中包含 EF Core 迁移的页面

作者:Tom DykstraJon P SmithRick Anderson

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

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

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

开发新应用时,数据模型会频繁更改。 每次模型更改时,模型都会与数据库不同步。 本教程系列首先将 Entity Framework 配置为创建数据库(如果不存在)。 每次数据模型更改时,都需要删除数据库。 下次运行应用时,调用 EnsureCreated 会重新创建数据库以匹配新的数据模型。 然后,该 DbInitializer 类运行以初始化新数据库。

若要使 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 不会创建迁移历史记录表,因此不能用于迁移。 它设计用于测试或快速原型制作,其中数据库被删除并经常重新创建。

从这一点开始,教程将使用迁移。

Program.cs中删除以下行:

context.Database.EnsureCreated();

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

升级与降级方法

命令 EF Coremigrations add 生成用于创建数据库的代码。 此迁移代码位于 Migrations\<timestamp>_InitialCreate.cs 文件中。 类 Up 的方法 InitialCreate 创建与数据模型实体集对应的数据库表。 该方法 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 迁移删除

请参阅 重置所有迁移 以删除所有迁移。

在生产环境中应用迁移

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

数据库迁移应作为部署的一部分进行,并采用受控的方式进行。 生产数据库迁移方法包括:

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

Troubleshooting

如果应用使用 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 与数据模型保持同步,在需要将应用部署到生产环境之前,此方法可正常运行。 当应用在生产环境中运行时,它通常存储需要维护的数据。 每次进行更改时,应用都无法从测试数据库开始(例如添加新列)。 迁移 EF Core 功能通过启用 EF Core 更新 DB 架构而不是创建新的数据库来解决此问题。

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

删除数据库

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

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

Drop-Database

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

创建初始迁移并更新 DB

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

Add-Migration InitialCreate
Update-Database

审查向上和向下的方法

命令 EF Coremigrations add 生成用于创建数据库的代码。 此迁移代码位于 Migrations\<timestamp>_InitialCreate.cs 文件中。 类 Up 的方法 InitialCreate 创建与数据模型实体集对应的数据库表。 该方法 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 已与数据模型匹配。

将应用部署到新环境时,必须运行数据库创建代码才能创建数据库。

以前删除了数据库,并且该数据库不存在,因此迁移将创建新的数据库。

数据模型快照

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

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

Remove-Migration

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

删除 EnsureCreated 并测试应用

对于早期开发,EnsureCreated 曾被使用。 在本教程中,将使用迁移。 EnsureCreated 具有以下限制:

  • 绕过迁移并创建 DB 和架构。
  • 不创建迁移表。
  • 不能与迁移同时使用。
  • 设计用于在进行测试或快速原型制作时,数据库会被删除并频繁重新创建。

删除 EnsureCreated

context.Database.EnsureCreated();

运行应用程序并验证数据库是否已初始化。

检查数据库

使用 SQL Server 对象资源管理器 检查数据库。 请注意 __EFMigrationsHistory 表的添加。 该 __EFMigrationsHistory 表用于跟踪已应用到数据库的迁移。 查看表中的数据 __EFMigrationsHistory ,它显示第一个迁移的一行。 上一个 CLI 输出示例中的最后一个日志显示创建此行的 INSERT 语句。

运行应用并验证一切是否正常工作。

在生产环境中应用迁移

建议生产应用 不应 在应用程序启动时调用 Database.MigrateMigrate 不应从位于服务器场中的应用程序进行调用。 例如,如果应用在云中进行了横向扩展部署(应用的多个实例正在运行)。

数据库迁移应作为部署的一部分进行,并采用受控的方式进行。 生产数据库迁移方法包括:

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

EF Core使用__MigrationsHistory表来查看是否需要运行任何迁移。 如果数据库是最新的,则不会执行任何迁移。

Troubleshooting

下载 已完成的应用

该应用生成以下异常:

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

解决方案:运行 dotnet ef database update

其他资源