用ASP.NET Core MVC 和 EF Core 构建Web应用 (九)

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

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

 

将继承映射到数据库表的选项

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

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

 

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

 

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

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

 

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

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

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

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

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

 

创建 Person 类

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

 1 using System.ComponentModel.DataAnnotations;
 2 using System.ComponentModel.DataAnnotations.Schema;
 3 
 4 namespace ContosoUniversity.Models
 5 {
 6     public abstract class Person
 7     {
 8         public int ID { get; set; }
 9 
10         [Required]
11         [StringLength(50)]
12         [Display(Name = "Last Name")]
13         public string LastName { get; set; }
14         [Required]
15         [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
16         [Column("FirstName")]
17         [Display(Name = "First Name")]
18         public string FirstMidName { get; set; }
19 
20         [Display(Name = "Full Name")]
21         public string FullName
22         {
23             get
24             {
25                 return LastName + ", " + FirstMidName;
26             }
27         }
28     }
29 }
View Code

 

使 Student 和 Instructor 类从 Person 继承

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

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class Instructor : Person
 9     {
10         [DataType(DataType.Date)]
11         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
12         [Display(Name = "Hire Date")]
13         public DateTime HireDate { get; set; }
14 
15         public ICollection<CourseAssignment> CourseAssignments { get; set; }
16         public OfficeAssignment OfficeAssignment { get; set; }
17     }
18 }
View Code

在 Student.cs 中做出相同更改。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class Student : Person
 9     {
10         [DataType(DataType.Date)]
11         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
12         [Display(Name = "Enrollment Date")]
13         public DateTime EnrollmentDate { get; set; }
14 
15 
16         public ICollection<Enrollment> Enrollments { get; set; }
17     }
18 }
View Code

 

将 Person 实体类型添加到数据模型

 1 using ContosoUniversity.Models;
 2 using Microsoft.EntityFrameworkCore;
 3 
 4 namespace ContosoUniversity.Data
 5 {
 6     public class SchoolContext : DbContext
 7     {
 8         public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
 9         {
10         }
11 
12         public DbSet<Course> Courses { get; set; }
13         public DbSet<Enrollment> Enrollments { get; set; }
14         public DbSet<Student> Students { get; set; }
15         public DbSet<Department> Departments { get; set; }
16         public DbSet<Instructor> Instructors { get; set; }
17         public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
18         public DbSet<CourseAssignment> CourseAssignments { get; set; }
19         public DbSet<Person> People { get; set; }
20 
21         protected override void OnModelCreating(ModelBuilder modelBuilder)
22         {
23             modelBuilder.Entity<Course>().ToTable("Course");
24             modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
25             modelBuilder.Entity<Student>().ToTable("Student");
26             modelBuilder.Entity<Department>().ToTable("Department");
27             modelBuilder.Entity<Instructor>().ToTable("Instructor");
28             modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
29             modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
30             modelBuilder.Entity<Person>().ToTable("Person");
31 
32             modelBuilder.Entity<CourseAssignment>()
33                 .HasKey(c => new { c.CourseID, c.InstructorID });
34         }
35     }
36 }
View Code

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

 

创建和自定义迁移代码

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

dotnet ef migrations add Inheritance

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

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

 1 protected override void Up(MigrationBuilder migrationBuilder)
 2 {
 3     migrationBuilder.DropForeignKey(
 4         name: "FK_Enrollment_Student_StudentID",
 5         table: "Enrollment");
 6 
 7     migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");
 8 
 9     migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
10     migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
11     migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
12     migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
13     migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);
14 
15     // Copy existing Student data into new Person table.
16     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");
17     // Fix up existing relationships to match new PK's.
18     migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");
19 
20     // Remove temporary key
21     migrationBuilder.DropColumn(name: "OldID", table: "Person");
22 
23     migrationBuilder.DropTable(
24         name: "Student");
25 
26     migrationBuilder.CreateIndex(
27          name: "IX_Enrollment_StudentID",
28          table: "Enrollment",
29          column: "StudentID");
30 
31     migrationBuilder.AddForeignKey(
32         name: "FK_Enrollment_Person_StudentID",
33         table: "Enrollment",
34         column: "StudentID",
35         principalTable: "Person",
36         principalColumn: "ID",
37         onDelete: ReferentialAction.Cascade);
38 }
View Code

 

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

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

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

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

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

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

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

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

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

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

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

运行 database update 命令:

dotnet ef database update

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

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

 

使用已实现的继承进行测试

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

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

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

 

总结

你已经为 PersonStudent 和 Instructor 类实现了每个层次结构一张表继承。 

 

 

 

 *****************************
 *** Keep learning and growing. ***
 *****************************
posted @ 2018-06-29 09:03  Gangle  阅读(266)  评论(0编辑  收藏  举报