使用 ASP.NET MVC 应用程序中的实体框架实现继承 (8(共 10 个) )

作者 :Tom Dykstra

Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 创建 ASP.NET MVC 4 应用程序。 若要了解教程系列,请参阅本系列中的第一个教程

注意

如果遇到无法解决的问题, 请下载已完成的章节 并尝试重现问题。 通常,可以通过将代码与已完成的代码进行比较来找到问题的解决方案。 有关一些常见错误及其解决方法,请参阅 错误和解决方法。

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

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

按层次结构表与按类型表继承

在面向对象的编程中,可以使用继承更轻松地使用相关类。 例如,Instructor数据模型中的 SchoolStudent 类共享多个属性,这会导致冗余代码:

显示学生和讲师课程的屏幕截图,其中突出显示了冗余代码。

假设想要消除由 InstructorStudent 实体共享的属性的冗余代码。 可以创建仅 Person 包含这些共享属性的基类,然后使 InstructorStudent 实体继承自该基类,如下图所示:

显示派生自 Person 类的学生和讲师课程的屏幕截图。

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

显示 Person 实体类继承结构的屏幕截图。

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

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

显示派生自 Person 实体类的新讲师和学生数据库表的屏幕截图。

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

TPH 继承模式通常比 TPT 继承模式在实体框架中提供更好的性能,因为 TPT 模式可能会导致复杂的联接查询。 本教程将演示如何实现 TPH 继承。 你将通过执行以下步骤来执行此操作:

  • 创建类Person,并将 和 Student 类更改为派生自 PersonInstructor
  • 将模型到数据库映射代码添加到数据库上下文类。
  • 在整个项目中将 和StudentID引用 更改为 。PersonIDInstructorID

创建 Person 类

注意:在更新使用这些类的控制器之前,创建以下类后将无法编译项目。

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

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

namespace ContosoUniversity.Models
{
   public abstract class Person
   {
      [Key]
      public int PersonID { get; set; }

      [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
      [StringLength(50, MinimumLength = 1)]
      [Display(Name = "Last Name")]
      public string LastName { get; set; }

      [Column("FirstName")]
      [Display(Name = "First Name")]
      [StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
      public string FirstMidName { get; set; }

      public string FullName
      {
         get
         {
            return LastName + ", " + FirstMidName;
         }
      }
   }
}

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

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

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

Student.cs 进行类似的更改。 类 Student 将如以下示例所示:

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

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

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

将人员实体类型添加到模型

SchoolContext.cs 中,为Person实体类型添加DbSet属性:

public DbSet<Person> People { get; set; }

以上是 Entity Framework 配置每个层次结构一张表继承所需的全部操作。 如你所看到的,重新创建数据库时,它将有一个 Person 表来代替 StudentInstructor 表。

将 InstructorID 和 StudentID 更改为 PersonID

SchoolContext.cs 的 Instructor-Course 映射语句中,更改为 MapRightKey("InstructorID")MapRightKey("PersonID")

modelBuilder.Entity<Course>()
    .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    .Map(t => t.MapLeftKey("CourseID")
    .MapRightKey("PersonID")
    .ToTable("CourseInstructor"));

此更改不是必需的;它只是更改多对多联接表中 InstructorID 列的名称。 如果将名称保留为 InstructorID,则应用程序仍将正常工作。 下面是已完成的 SchoolContext.cs

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
      public DbSet<Person> People { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("PersonID")
                 .ToTable("CourseInstructor"));
      }
   }
}

接下来,需要在整个项目中将 PersonID 更改为 InstructorIDPersonIDStudentID,迁移文件夹中的时间戳迁移文件除外。 为此,只需找到并打开需要更改的文件,然后对打开的文件执行全局更改。 迁移 文件夹中应 更改的唯一文件是 Migrations\Configuration.cs。

  1. 重要

    首先,在 Visual Studio 中关闭所有打开的文件。

  2. 单击“查找和替换” -- 在“编辑”菜单中查找所有文件,然后搜索包含 InstructorID的项目中的所有文件。

    显示“查找和替换”窗口的屏幕截图。“讲师 ID”、“当前项目”、“匹配大小写”和“匹配全字”复选框以及“全部查找”按钮均突出显示。

  3. 通过双击每个文件的一行,打开“查找结果”窗口中除 Migrations 文件夹中的 time-stamp>_.cs 迁移文件以外的<每个文件。

    显示“查找结果”窗口的屏幕截图。时间戳迁移文件以红色划掉。

  4. 打开“ 替换文件中” 对话框,并将 “查找范围 ”更改为 “所有打开的文档”。

  5. 使用“ 在文件中替换 ”对话框将全部 InstructorID 更改为 PersonID.

    显示“查找和替换”窗口的屏幕截图。在“替换为文本”字段中输入人员 ID。

  6. 查找项目中包含 StudentID的所有文件。

  7. 通过双击每个文件的一行,打开“查找结果”窗口中除“迁移”文件夹中的 time-stamp>_*.cs 迁移文件以外的<每个文件。

    显示“查找结果”窗口的屏幕截图。时间戳迁移文件将被划掉。

  8. 打开“ 替换文件中” 对话框,并将 “查找范围 ”更改为 “所有打开的文档”。

  9. 使用 “ 在文件中替换 ” 对话框,将所有 StudentIDPersonID更改为 。

    显示“查找和替换”窗口的屏幕截图。突出显示了“文件”中的“替换”、“所有打开的文档”、“匹配大小写”和“全字匹配”复选框,以及“全部替换”按钮。

  10. 生成项目。

(请注意,这演示了命名主键的模式的classnameID缺点。如果命名的主键 ID 没有为类名添加前缀,则现在无需重命名。)

创建和更新迁移文件

在包管理器控制台 (PMC) 中,输入以下命令:

Add-Migration Inheritance

Update-Database PMC 中运行 命令。 此时,命令将失败,因为我们有迁移不知道如何处理的现有数据。 收到以下错误:

ALTER TABLE 语句与 FOREIGN KEY 约束“FK_dbo”冲突。Department_dbo。Person_PersonID”。 冲突发生在数据库“ContosoUniversity”表“dbo 中。Person“,列”PersonID”。

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

public override void Up()
{
    DropForeignKey("dbo.Department", "InstructorID", "dbo.Instructor");
    DropForeignKey("dbo.OfficeAssignment", "InstructorID", "dbo.Instructor");
    DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
    DropForeignKey("dbo.CourseInstructor", "InstructorID", "dbo.Instructor");
    DropIndex("dbo.Department", new[] { "InstructorID" });
    DropIndex("dbo.OfficeAssignment", new[] { "InstructorID" });
    DropIndex("dbo.Enrollment", new[] { "StudentID" });
    DropIndex("dbo.CourseInstructor", new[] { "InstructorID" });
    RenameColumn(table: "dbo.Department", name: "InstructorID", newName: "PersonID");
    RenameColumn(table: "dbo.OfficeAssignment", name: "InstructorID", newName: "PersonID");
    RenameColumn(table: "dbo.Enrollment", name: "StudentID", newName: "PersonID");
    RenameColumn(table: "dbo.CourseInstructor", name: "InstructorID", newName: "PersonID");
    CreateTable(
        "dbo.Person",
        c => new
            {
                PersonID = c.Int(nullable: false, identity: true),
                LastName = c.String(maxLength: 50),
                FirstName = c.String(maxLength: 50),
                HireDate = c.DateTime(),
                EnrollmentDate = c.DateTime(),
                Discriminator = c.String(nullable: false, maxLength: 128),
                OldId = c.Int(nullable: false)
            })
        .PrimaryKey(t => t.PersonID);

    // Copy existing Student and Instructor data into new Person table.
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, StudentId AS OldId FROM dbo.Student");
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, HireDate, null AS EnrollmentDate, 'Instructor' AS Discriminator, InstructorId AS OldId FROM dbo.Instructor");

    // Fix up existing relationships to match new PK's.
    Sql("UPDATE dbo.Enrollment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Enrollment.PersonId AND Discriminator = 'Student')");
    Sql("UPDATE dbo.Department SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Department.PersonId AND Discriminator = 'Instructor')");
    Sql("UPDATE dbo.OfficeAssignment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = OfficeAssignment.PersonId AND Discriminator = 'Instructor')");
    Sql("UPDATE dbo.CourseInstructor SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = CourseInstructor.PersonId AND Discriminator = 'Instructor')");

    // Remove temporary key
    DropColumn("dbo.Person", "OldId");

    AddForeignKey("dbo.Department", "PersonID", "dbo.Person", "PersonID");
    AddForeignKey("dbo.OfficeAssignment", "PersonID", "dbo.Person", "PersonID");
    AddForeignKey("dbo.Enrollment", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
    AddForeignKey("dbo.CourseInstructor", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
    CreateIndex("dbo.Department", "PersonID");
    CreateIndex("dbo.OfficeAssignment", "PersonID");
    CreateIndex("dbo.Enrollment", "PersonID");
    CreateIndex("dbo.CourseInstructor", "PersonID");
    DropTable("dbo.Instructor");
    DropTable("dbo.Student");
}

再次运行命令 update-database

注意

迁移数据和更改架构时可能会收到其他错误。 如果遇到无法解决的迁移错误,可以通过更改 Web.config文件中的连接字符串或删除数据库来继续学习本教程。 最简单的方法是重命名 Web.config 文件中的数据库。 例如,将数据库名称更改为 CU_test,如以下示例所示:

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
      Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf" 
      providerName="System.Data.SqlClient" />

使用新数据库时,没有要迁移的数据,并且 update-database 该命令更有可能在没有错误的情况下完成。 有关如何删除数据库的说明,请参阅 如何从 Visual Studio 2012 中删除数据库。 如果采用此方法以继续学习本教程,请跳过本教程末尾的部署步骤,因为部署的站点在自动运行迁移时会收到相同的错误。 如果要排查迁移错误,最佳资源是实体框架论坛之一或 StackOverflow.com。

测试

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

“服务器资源管理器”中, 展开“ SchoolContext ”,然后展开“ ”,可以看到 “学生 ”表和 “讲师” 表已替换为 “人员 ”表。 展开 “人员” 表,可以看到它包含“ 学生 ”和 “讲师 ”表中过去的所有列。

显示“服务器资源管理器”窗口的屏幕截图。展开“数据连接”、“学校上下文”和“表”选项卡以显示“人员”表。

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

显示“人员”表的屏幕截图。突出显示了鉴别器列名称。

下图演示了新 School 数据库的结构:

显示 School 数据库关系图的屏幕截图。

总结

现已为 PersonStudentInstructor 类实现按层次结构的表继承。 有关此继承结构和其他继承结构的详细信息,请参阅 Morteza Manavi 博客上的 继承映射策略 。 在下一教程中,你将了解实现存储库和工作单元模式的一些方法。

可以在 ASP.NET 数据访问内容映射中找到指向其他实体框架资源的链接。