教程:实现继承 - ASP.NET MVC 和 EF Core

在上一个教程中,已经处理了并发异常。 本教程将演示如何在数据模型中实现继承。

在面向对象的编程中,可以使用继承以便于重用代码。 在本教程中,将更改 InstructorStudent 类,以便从 Person 基类中派生,该基类包含教师和学生所共有的属性(如 LastName)。 不会添加或更改任何网页,但会更改部分代码,并将在数据库中自动反映这些更改。

在本教程中,你将了解:

  • 会将继承映射到数据库
  • 创建 Person 类
  • 更新 Instructor 和 Student
  • 向模型添加 Person
  • 创建和更新迁移
  • 测试实现

先决条件

会将继承映射到数据库

学校数据模型中的 InstructorStudent 类具有多个相同的属性:

Student and Instructor classes

假设想要消除由 InstructorStudent 实体共享的属性的冗余代码。 或者想要写入可以格式化姓名的服务,而无需关注该姓名来自教师还是学生。 可以创建只包含这些共享属性的 Person 基类,然后使 InstructorStudent 类继承该基类,如下图所示:

Student and Instructor classes deriving from Person class

有多种方法可以在数据库中表示此继承结构。 可以创建一个 Person 表,将学生和教师的相关信息包含在一个表中。 某些列可能仅适用于教师 (HireDate),某些列仅适用于学生 (EnrollmentDate),某些列同时适用于两者(LastName、FirstName)。 通常情况下,将有一个鉴别器列来指示每行所代表的类型。 例如,鉴别器列可能包含“Instructor”来指示教师,包含“Student”来指示学生。

Table-per-hierarchy example

从单个数据库表生成实体继承结构的模式称为“每个层次结构一张表 (TPH)”继承。

另一种方法是使数据库看起来更像继承结构。 例如,可以仅将姓名字段包含到 Person 表中,在单独的 InstructorStudent 表中包含日期字段。

警告

EF Core 3.x 不支持每个类型一张表 (TPT),但 TPT 已在 EF Core 5.0 中实现。

Table-per-type inheritance

为每个实体类创建数据库表的模式称为“每个类型一张表 (TPT)”继承。

另一种方法是将所有非抽象类型映射到单独的表。 类的所有属性(包括继承的属性)映射到相应表的列。 此模式称为“每个具体类一张表 (TPC)”继承。 如果为前面所示的 PersonStudentInstructor 类实现了 TPC 继承,那么在实现继承之后,StudentInstructor 表看起来将与以前没什么不同。

TPC 和 TPH 继承模式的性能通常比 TPT 继承模式好,因为 TPT 模式会导致复杂的联接查询。

本教程将演示如何实现 TPH 继承。 TPH 是 Entity Framework Core 唯一支持的继承模式。 需要执行的操作是创建 Person 类、将 InstructorStudent 类更改为从 Person 派生、将新的类添加到 DbContext,以及创建迁移。

提示

在进行以下更改之前,请考虑保存项目的副本。 如果遇到问题并需要重新开始,可以更轻松地从已保存的项目开始,而不用反向操作本教程中的步骤或者返回到整个系列的开始。

创建 Person 类

在 Models 文件夹中,创建 Person.cs 并使用以下代码替换模板代码:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public abstract class Person
    {
        public int ID { get; set; }

        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}

更新 Instructor 和 Student

Instructor.cs 中,从 Person 类派生 Instructor 类并删除键和姓名字段。 代码将如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Student.cs 中做出相同更改。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }


        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

向模型添加 Person

将 Person 实体类型添加到 SchoolContext.cs。 新的行突出显示。

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
            modelBuilder.Entity<Person>().ToTable("Person");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

以上是 Entity Framework 配置每个层次结构一张表继承所需的全部操作。 正如将看到的,更新数据库时,将有一个 Person 表来代替 Student 和 Instructor 表。

创建和更新迁移

保存更改并生成项目。 随后在项目文件夹中打开命令窗口并输入以下命令:

dotnet ef migrations add Inheritance

暂不运行 database update 命令。 该命令将导致数据丢失,因为它将删除 Instructor 表并将 Student 表重命名为 Person。 需要提供自定义代码来保留现有数据。

打开 Migrations/<timestamp>_Inheritance.cs 并将 Up 方法替换为以下代码:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_Enrollment_Student_StudentID",
        table: "Enrollment");

    migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

    migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
    migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
    migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

    // Copy existing Student data into new Person table.
    migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
    // Fix up existing relationships to match new PK's.
    migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    migrationBuilder.DropColumn(name: "OldID", table: "Person");

    migrationBuilder.DropTable(
        name: "Student");

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

    migrationBuilder.AddForeignKey(
        name: "FK_Enrollment_Person_StudentID",
        table: "Enrollment",
        column: "StudentID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);
}

此代码负责以下数据库更新任务:

  • 删除指向 Student 表的外键约束和索引。

  • 将 Instructor 表重命名为 Person,根据需要做出更改以存储学生数据:

  • 为学生添加可为 NULL 的 EnrollmentDate。

  • 添加鉴别器列来指示行代表学生还是教师。

  • HireDate 可为 NULL,因为学生行不会包含聘用日期。

  • 添加临时字段,用于更新指向学生的外键。 将学生复制到 Person 表时,将获取新的主键值。

  • 将数据从 Student 表复制到 Person 表。 这将使学生获取分配的新主键值。

  • 修复指向学生的外键值。

  • 重新创建外键约束和索引,现在将它们指向 Person 表。

(如果已使用 GUID 而不是整数作为主键类型,那么将不需要更改学生主键值,并且可能已省略其中多个步骤。)

运行 database update 命令:

dotnet ef database update

(在生产系统中,可以对 Down 方法进行相应更改,以防必须使用该方法返回到以前的数据库版本。对于本教程,将不使用 Down 方法。)

注意

在包含现有数据的数据库中更改架构时,可能会发生其他错误。 如果出现无法解决的迁移错误,可以在连接字符串中更改数据库名或者删除数据库。 若是新数据库,则没有要迁移的数据,因此在完成更新数据库命令时很可能不会出错。 若要删除数据库,请使用 SSOX 或运行 database drop CLI 命令。

测试实现

运行应用并尝试各种页面。 一切都和以前一样。

在“SQL Server 对象资源管理器” 中,展开“数据连接/SchoolContext”和“表”,将看到 Student 和 Instructor 表已替换为 Person 表。 打开 Person 表设计器,将看到它包含在 Student 和 Instructor 表中使用的所有列。

Person table in SSOX

右键单击 Person 表,然后单击“显示表数据”以查看鉴别器列。

Person table in SSOX - table data

获取代码

下载或查看已完成的应用程序。

其他资源

若要详细了解 Entity Framework Core 中的继承,请参阅继承

后续步骤

在本教程中,你将了解:

  • 已将继承映射到数据库
  • 已创建 Person 类
  • 已更新 Instructor 和 Student
  • 已向模型添加 Person
  • 已创建和更新迁移
  • 已测试实现

请继续阅读下一篇教程,了解如何处理各种相对高级的 Entity Framework 方案。