第二章 实体数据建模基础之继承关系映射TPH

2-10 Table per Hierarchy Inheritance 建模

问题

你有这样一张数据库表,有一类型或 鉴别 列。它能判断行中的数据在你的应用中代表的是什么。你想使用table per hierarchy(TPH)继承映射建模。

解决方案

让我们假设你有如图2-20中的表(译注:总感觉作者使用的图,跟实际描述对不上,比如下图应该是实体模型图),Employee表包含hourly employees 和salaried employees的行。列EmployeeType作为鉴别列,鉴别这两种员工类型的行。 当EmployeType为1时,这一行代表一个专职员工(salaried or full-time employee),当值为2时,这一行代码一个钟点工(hourly employee).

图2-20 一个包含hourly employees 和salaried employees的表 Employee

按下面的步骤,使用TPH基于表Employee建模:

1、在你的项目中创建一个继承自DbContext的上下文对象EF6RecipesContext;

2、使用代码清单2-21创建一个抽象的POCO实体Employee;

代码清单2-21.创建一个抽象的POCO实体Employee

[Table("Employee", Schema = "Chapter2")]
public abstract class Employee {
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int EmployeeId { get; protected set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

3、使用代码清单2-22创建一个继承至Emplyee的POCO实体类FullTimeEmployee.

代码清单2-22. 创建一个POCO实体类FullTimeEmployee

public class FullTimeEmployee : Employee
{
public decimal? Salary { get; set; }
}

4、使用代码清单2-23创建一个继承至Emplyee的POCO实体类HourlyEmployee.

代码清单2-23. 创建一个POCO实体类HourlyEmployee

public class HourlyEmployee : Employee {
    public decimal? Wage { get; set; }
}

5、在上下文中添加一个类型为DbSet<Employee>的属性。

6、在上下文中重写方法OnModelCreating,在方法中映射你的具体的employee类型到EmployeeType鉴别列。如代码清单2-24所示.

代码清单2-24. 重写上下文中的OnModelCreating方法

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Employee>()
    .Map<FullTimeEmployee>(m => m.Requires("EmployeeType").HasValue(1))
    .Map<HourlyEmployee>(m => m.Requires("EmployeeType").HasValue(2));
}


注意:非共享属性(例如:Salary和Wage)必须为可空类型。

原理

在table per hierarchy(通常缩写为TPH)继承映射中,用一张单独的表代表整个继承层次。不像TPT,TPH同时把基类和派生类混合进同一张表的行。它们依据鉴别列来区分。在我们示例中,鉴别列是EmployeeType。

在TPH中,我们设置实体配置中的映射条件,用来指明鉴别列的值让表映射到不同的派生类型。我们让基类为抽象类型。通过设置其为抽象类型,我就不用提供一个映射条件,因为一个抽象的实体是不会被创建的。我们永远不会有一个Employee实体的实例。我们不用在Employee实体中实现一个EmployeeType属性。列不会用来作映射条件,一般是映射到一个属性。

代码清单2-25演示从模型插入和获取数据。

代码清单2-25 在我们的TPH模型中插入和获取数据

using (var context = new EF6RecipesContext()) {
    var fte = new FullTimeEmployee {
        FirstName = "Jane",
        LastName = "Doe",
        Salary = 71500M
    };
    context.Employees.Add(fte);
    fte = new FullTimeEmployee {
        FirstName = "John",
        LastName = "Smith",
        Salary = 62500M
    };
    context.Employees.Add(fte);
    var hourly = new HourlyEmployee {
        FirstName = "Tom",
        LastName = "Jones",
        Wage = 8.75M
    };
    context.Employees.Add(hourly);
    context.SaveChanges();
}
using (var context = new EF6RecipesContext()) {
    Console.WriteLine("--- All Employees ---");
    foreach (var emp in context.Employees) {
        bool fullTime = emp is HourlyEmployee ? false : true;
        Console.WriteLine("{0} {1} ({2})", emp.FirstName, emp.LastName,
        fullTime ? "Full Time" : "Hourly");
    }
    Console.WriteLine("--- Full Time ---");
    foreach (var fte in context.Employees.OfType<FullTimeEmployee>()) {
        Console.WriteLine("{0} {1}", fte.FirstName, fte.LastName);
    }
    Console.WriteLine("--- Hourly ---");
    foreach (var hourly in context.Employees.OfType<HourlyEmployee>()) {
        Console.WriteLine("{0} {1}", hourly.FirstName, hourly.LastName);
    }
}

代码清单的输出为:

--- All Employees ---Jane Doe (Full Time)
John Smith (Full Time)
Tom Jones (Hourly)
--- Full Time ---Jane Doe
John Smith
--- Hourly ---Tom Jones


代码清单2-15,创建、初始化、添加两个full-time employees和一个hourly employee.在查询中,我们获取所有的employees,用is操作符来判断employee是我们拥有员工类型中的哪一种。当我打印出员工姓名时,我们指出他的类型。

在代码块中,我们使用泛型方法OfType<T>()获取full-time employees和hourly employees.

最佳实践

在TPH继承映射中,什么时候使用抽象基类,什么时候在实体中创建一个映射条件,存在着争论。使用一个具体的基类的难点在,查询出整个继承中的实例,非常的繁琐。这里的最佳实践是,如果你的应用中不需要基类实体的实例,那么让它成为抽象类型。

如果你的应用中需要一个基类的实例,可以考虑引进一个新的继承实体来覆盖基类中的映射条件属性。例如,在上例中我们可以创建一个这样的派生类UnclassifiedEmployee。 一旦有这个派生类后,我们就可以放心地把基类设为抽象类型。这就提供了一种简单的方式来规避通过在基类中使用映射条件属性来查询的问题。

在使用TPH时,有几条准则需要记住。第一点,映射条件属性值必须相互独立。换句话来说,就是你不能将一行,条件映射到两个或是更多的类型上。

第二点,映射条件必须对表中的每一行负责,不能存在某一行不被映射到合适的实体类型上。如果你的系统是一个遗留的数据库,且表中的行由别的系统来创建,你没有机会条件映射,这种情况会相当的麻烦。 这会发生什么状况呢?不能映射到基类或派生类的行,将不能被模型访问到

第三点,鉴别列不能映射到一个实体属性上,除非它先被用作一个is not null的映射条件。这一点看上去有点过分严格。你可能会问,“如果不能设置鉴别值,那怎么插入一行代表派生类的数据?” ,答案很简洁,你直接创建一个派生类的实例,然后和添加别的实体类型实例一样,将其添加到上下文中,对象服务会创建一行拥有合适的鉴别值的插入语句。

《Entity Framework 6 Recipes》中文翻译系列