教程:在 ASP.NET MVC 5 应用中使用 EF 实现继承

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

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

在本教程中,你将了解:

  • 了解如何将继承映射到数据库
  • 创建 Person 类
  • 更新 Instructor 和 Student
  • 将人员添加到模型
  • 创建和更新迁移
  • 测试实现
  • 部署到 Azure

先决条件

会将继承映射到数据库

Instructor数据模型中的 SchoolStudent 类具有几个完全相同的属性:

Student_and_Instructor_classes

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

Student_and_Instructor_classes_deriving_from_Person_class

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

每hierarchy_example表

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

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

每type_inheritance表

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

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

TPC 和 TPH 继承模式通常比 TPT 继承模式在实体框架中提供更好的性能,因为 TPT 模式可能会导致复杂的联接查询。

本教程将演示如何实现 TPH 继承。 TPH 是实体框架中的默认继承模式,因此只需创建一个Person类,将 和 Student 类更改为Instructor派生自 Person,将新类添加到 DbContext,然后创建迁移。 (有关如何实现其他继承模式的信息,请参阅 MSDN 实体框架文档中 的映射按类型表 (TPT) 继承映射每个具体表类 (TPC) 继承 。)

创建 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.csStudent.cs 以从 Person.sc 继承值。

Instructor.csInstructor ,从 类派生类, 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 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;
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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

将人员添加到模型

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

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

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

创建和更新迁移

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

Add-Migration Inheritance

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

无法删除对象 'dbo。讲师',因为它由 FOREIGN KEY 约束引用。

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

public override void Up()
{
    // Drop foreign keys and indexes that point to tables we're going to drop.
    DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
    DropIndex("dbo.Enrollment", new[] { "StudentID" });

    RenameTable(name: "dbo.Instructor", newName: "Person");
    AddColumn("dbo.Person", "EnrollmentDate", c => c.DateTime());
    AddColumn("dbo.Person", "Discriminator", c => c.String(nullable: false, maxLength: 128, defaultValue: "Instructor"));
    AlterColumn("dbo.Person", "HireDate", c => c.DateTime());
    AddColumn("dbo.Person", "OldId", c => c.Int(nullable: true));

    // Copy existing Student 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, ID AS OldId FROM dbo.Student");

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

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

    DropTable("dbo.Student");

    // Re-create foreign keys and indexes pointing to new table.
    AddForeignKey("dbo.Enrollment", "StudentID", "dbo.Person", "ID", cascadeDelete: true);
    CreateIndex("dbo.Enrollment", "StudentID");
}

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

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

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

    • 为学生添加可为 NULL 的 EnrollmentDate。
    • 添加鉴别器列来指示行代表学生还是教师。
    • HireDate 可为 NULL,因为学生行不会包含聘用日期。
    • 添加临时字段,用于更新指向学生的外键。 将学生复制到 Person 表中时,他们将获得新的主键值。
  • 将数据从 Student 表复制到 Person 表。 这将使学生获取分配的新主键值。

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

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

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

再次运行命令 update-database

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

注意

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

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

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

测试实现

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

“服务器资源管理器”中, 展开 “数据连接\SchoolContext ”,然后展开“ ”,可以看到 StudentInstructor 表已替换为 Person 表。 展开 “人员” 表,可以看到其中包含“ 学生 ”和“ 讲师 ”表中的所有列。

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

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

School_database_diagram

部署到 Azure

本部分要求完成本教程系列的第 3 部分“排序、筛选和分页”中的可选“将应用部署到 Azure”部分。 如果出现通过删除本地项目中的数据库解决的迁移错误,请跳过此步骤;或创建新的站点和数据库,并部署到新环境。

  1. 在 Visual Studio 中,在“解决方案资源管理器”中右键单击项目,并从上下文菜单中选择“发布”。

  2. 单击“发布” 。

    Web 应用将在默认浏览器中打开。

  3. 测试应用程序以验证它是否正常工作。

    首次运行访问数据库的页面时,实体框架将运行使数据库与当前数据模型保持最新所需的所有迁移 Up 方法。

获取代码

下载已完成项目

其他资源

可以在 ASP.NET 数据访问 - 推荐资源中找到指向其他实体框架资源的链接。

有关此继承结构和其他继承结构的详细信息,请参阅 MSDN 上的 TPT 继承模式TPH 继承模式 。 下一个教程将介绍如何处理各种相对高级的 Entity Framework 方案。

后续步骤

在本教程中,你将了解:

  • 已了解如何将继承映射到数据库
  • 已创建 Person 类
  • 已更新 Instructor 和 Student
  • 向模型添加了人员
  • 创建和更新迁移
  • 已测试实现
  • 部署到 Azure

请转到下一篇文章,了解在了解开发使用实体框架代码优先的 web 应用程序 ASP.NET 基础知识时需要注意的主题。