继承 - EF Core 与 ASP.NET Core MVC 教程 (9 of 10)
作者 Tom Dykstra 、 Rick Anderson
Contoso 大学 Web应用程序演示了如何使用 Entity Framework Core 1.1 以及 Visual Studio 2017 来创建 ASP.NET Core 1.1 MVC Web 应用程序。更多信息请参考 第一节教程.
在之前的教程中你已经学习了如何更新数据。本教程将指导你当多个用户同时更新同一实体时如何处理冲突。
在面向对象编程(OOP,object-oriented programming)中,可以利用继承来方便代码复用。在本教程中你将改变 Instructor
和 Student
类,以使其派生自 Person
基类,该基类包含了诸如 LastName
等同时存在于教师与学生中的属性。你不需要添加或改变你现有的任何网页,只需要改变代码并使这些改变自动反映到数据库之中。
为数据库表配置映射继承
School 数据模型中的 Instructor
和 Student
具有如下几个共同属性:
假设你希望消除 Instructor
和 Student
实体之间共有属性的冗余代码,或者你想编写一个无关教师或学生的服务。你可以创建一个 Person
基类来包含这些共有的属性,然后把 Instructor
和 Student
类改为从该基类派生,如下图所示:
这一继承结构在数据库中的表示可能有多种方式。你可以有一张包含学生与教师共有信息的单一表格 Person 表。一些列可能只适合用于教师(HireDate时间),一些列可能只适合用于学生(EnrollmentDate),也有一些两者都适用(LastName 和 FirstName)。通常来说你可以有一个鉴别列用来指示每一行属于哪种类型。比如用「Instructor」表示这行是教师数据,用「Student」表示这是学生数据。
生成从单个数据库表结构继承而来的实体的模式叫做 TPH(table-per-hierarchy) 继承。
另一个方式是让数据库更像是继承结构。比如可以有一个只有姓名字段的 Person 表,以及两个相互独立的带有日期字段的 Instructor 和 Student 表。
为每个实体类创建数据库表的模式叫做 TPT(table-per-type)继承。
另一种可以选择的方法时映射所有非抽象类型(non-abstract types)为单张表。类的每一个属性(包括它所继承到的属性)都映射到表的相应字段。这一模式被叫做 TPC(table-per-concrete)。如果你为 Person、Student 和 Instructor 类实现 TPC 继承(如前所示),实现了继承的 Student 和 Instructor 表看起来和先前的不会有什么不同。
生成从单个数据库表结构继承而来的实体的模式叫做 TPH(table-per-hierarchy) 继承。
本教程演示如何实现 TPH 继承。TPH 是 Entity Framework Core 所支持的唯一一种继承模式。你所需要做的是创建 Person
类,然后将 Instructor
and和Student
类改为派生自 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;
}
}
}
}
使 Student 与 Instructor 类继承自 Person 类
在 Instructor.cs 中,将 Instructor 改为派生自 Person 类,并移除键和名称字段。代码看上去如下所示:
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 需要为配置 TPH 继承所做的全部工作。如你所见,当数据库更新,将有一个 Person 表来取代 Student 和 Instructor 表。
创建并定制迁移代码
保存变更并构建项目。从项目文件夹中打开命令窗口,然后输入以下命令:
dotnet ef migrations add Inheritance
运行 database update
命令:
dotnet ef database update
命令将会失败在这一点上,因为你有一些存在的数据,而迁移并不知道该如何处理它们。你会获得类似如下遮掩的错误信息:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_CourseAssignment_Person_InstructorID". The conflict occurred in database "ContosoUniversity09133", table "dbo.Person", column 'ID'.
打开 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,并根据需要更新存储在 Student 的数据:
- 在 Students 表中增加可空的 EnrollmentDate。
- 添加 Discriminator 列用于明确这行记录时学生还是教师。
- 将 HireDate 设置为可空,因为学生记录中不会有雇用时间。
- 增加一个临时字段用于更新指向学生的外键。当你复制学生的数据到 Person 表时,它们会获取新的主键值。
- 从 Student 表中复制数据到 Person 表。这将会导致学生获取新的被分配的主键值。
- 修复指向学生的外键值。
- 重新创建外键约束和索引,现在它们指向 Person 表。
(如果你的铸件类型是 GUID 而不是整形数字,那么学生的主键值并不需要更新,那么这些步骤也就能省了。)
再次运行 database update
命令:
dotnet ef database update
(在生产系统中,你需要对 Down
方法做相应的变更,这样万一需要,你可以用它回滚到之前的数据库版本。在本教程中你不会用到 Down
方法。)
备注
有可能在给一个已存在数据的数据库架构做更新时会获得其他错误。如果你得到了解决不了的迁移错误,你可以修改连接字符串中的数据库名称,或者删除数据库。在新数据库中,没有数据需要被迁移,update-database 命令更多的会无错完成。要删除数据库,可以使用 SSOX 或者使用 database drop
这个 CLI 命令。
测试继承实现
测试继承实现
运行站点,然后尝试访问不同的页面。现在一切正常如故。
在 SQL Server Object Explorer 中依次展开 Data Connections/SchoolContext 和 Tables,然后你能看到 Student 与 Instructor 表已被 Person 表所取代。打开 Person 表设计器,你可以发现它的所有列之前被用于 Student 和 Instructor 表。
右键点击 Person 表,然后点击 Show Table Data 来辨别列。
总结
你已经给 Person
, Student
, 以及 Instructor
类实现了 TPH(table-per-hierarchy,每个层次结构一张表)。更多有关 Entity Framework 继承的资料请阅读 继承。在下一篇教程中,我们将介绍如何处理各种 Entity Framework 高级场景。