By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.
原文地址:http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/implementing-inheritance-with-the-entity-framework-in-an-asp-net-mvc-application
全文目錄:Contoso 大學 - 使用 EF Code First 創建 MVC 應用
在上一次教程中,你已經能夠處理並發異常。這個教程將會展示如何在數據模型中實現繼承。
在面向對象的程序設計中,你可以通過繼承來清除冗余的代碼。在這個教程中,你將要通過修改教師 Instructor 和學生 Student 類,以便使他們從包含類似 LastName 屬性的 Person 類中派生。對於 Web 頁面不需要任何改動,你需要修改一點代碼,這些修改將會被自動反射到數據庫中。
8-1 單表繼承 (Table-per-Hierarchy) 對類型表 (Table-per-Type) 繼承
在面向對象的程序設計中,你可以通過對相關的類使用繼承來使得工作更加簡單。例如,教師 Instructor 和學生 Student 類在學校 School 數據模型中共享多個屬性,帶來了冗余的代碼。

假設你希望清除在教師 Instructor 和學生 Student 之間所共享的屬性帶來的冗余代碼。可以創建一個 Person 基類,其中僅僅包含他們共享的屬性,然后,使得教師 Instructor 和學生 Student 類從 Person 基類派生,如下圖所示。
在數據庫中這種繼承結構可以有多種表現形式,可以創建一個名為 Person 的表,在這個獨立的表中包含教師和學生所有的信息。既包括他們獨自擁有的屬性 ( 例如教師的 HireDate ,以及學生的 EnrollmentDate ),也包括它們共有的屬性 ( 例如 LastName, FirstName )。通常你還需要一個用於識別當前類型的列 discriminator 來標識當前行的類型。( 在這里,標識列的內容為 Instructor 來表示教師,Student 來表示學生 )

使用單個數據庫表來生成實體繼承結構的模式稱為單表繼承模式 TPH (table-per-hierarchy )。
另外一種方式是使得數據庫看起來類似繼承結構。例如,在 Person 表中僅僅包含他們共有的名字屬性,而將不同的時間分別保存到獨立的 Instructor 和 Student 表中。
這種每種實體類對應一張數據庫表的模式稱為類型表 TPT 繼承 (table per type )。
在 EF 中,TPH 繼承比 TPT 繼承有更好的性能,因為 TPT 繼承需要復雜的連接查詢。這個教程演示如何實現 TPH 繼承。你需要完成如下的步驟:
- 創建 Person 類,將 Instructor 和 Student 類從 Person 類中派生
- 在數據庫上下文類中增加模型到數據庫的映射代碼
- 將項目中的 InstructorID 和 StudentID 修改為使用 PersonID.
8-2 創建 Person 類
在 Model 文件夾中,創建 Person.cs ,使用下面的代碼替換原有的代碼。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ContosoUniversity.Models { public abstract class Person { [Key] public int PersonID { get; set; } [Required(ErrorMessage = "Last name is required.")] [Display(Name="Last Name")] [MaxLength(50)] public string LastName { get; set; } [Required(ErrorMessage = "First name is required.")] [Column("FirstName")] [Display(Name = "First Name")] [MaxLength(50)] public string FirstMidName { get; set; } public string FullName { get { return LastName + ", " + FirstMidName; } } } }
在 Instructor.cs 文件中,將 Instructor 從 Person 派生出來,刪除 key 和 name 字段。代碼如下所示。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ContosoUniversity.Models { public class Instructor : Person { [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)] [Required(ErrorMessage = "Hire date is required.")] [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 { [Required(ErrorMessage = "Enrollment date is required.")] [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)] [Display(Name = "Enrollment Date")] public DateTime? EnrollmentDate { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } } }
8-3 在模型中增加 Person 實體類型
在 SchoolContext.cs 文件中,對 Person 實體類型增加一個 DbSet 類型的屬性。
public DbSet<Person> People { get; set; }
這就是在 EF 中配置 TPH 繼承所有需要完成的工作。如你所見,當數據庫重建的時候,EF 會自動創建 Person 表。
8-4 修改 InstructorID 和 StudentID 為 PersonID
在 SchoolContext.cs 文件中,在教師對課程的映射語句中,將 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,程序還是可以正常工作的。
下一步,進行一次全局的替換 ( 項目中所有的文件 ),將 InstructorID 修改為 PersonID,StudentID 修改為 PersonID。注意區分大小寫。( 這里演示了使用類名加上 ID 作為主鍵的一個缺點。如果你沒有使用類名加上 ID 作為主鍵,這里就不需要重新命名 )
8-5 在初始化器中修改主鍵的值
在 SchoolInitializer.cs 中,代碼假設對於 Student 和 Instructor 實體的主鍵數字是分離的。這對於 Student 實體來說仍然正確 ( 他們仍然從 1 到 8 ),但是對於 Instructor 實體來說,將不再從 1-5 而是修改為 9-13,因為在初始化類中的代碼將教師增加在同一張表的學生之后。使用下面的代碼修改 Department 和 OfficeAssignment 實體中的代碼,以便使用新的教師 ID。
var departments = new List<Department> { new Department { Name = "English", Budget = 350000, StartDate = DateTime.Parse("2007-09-01"), PersonID = 9 }, new Department { Name = "Mathematics", Budget = 100000, StartDate = DateTime.Parse("2007-09-01"), PersonID = 10 }, new Department { Name = "Engineering", Budget = 350000, StartDate = DateTime.Parse("2007-09-01"), PersonID = 11 }, new Department { Name = "Economics", Budget = 100000, StartDate = DateTime.Parse("2007-09-01"), PersonID = 12 } };
var officeAssignments = new List<OfficeAssignment> { new OfficeAssignment { PersonID = 9, Location = "Smith 17" }, new OfficeAssignment { PersonID = 10, Location = "Gowan 27" }, new OfficeAssignment { PersonID = 11, Location = "Thompson 304" }, };
8-6 將 OfficeAssignment 調整為延遲加載
當前版本的 EF 對導航屬性使用了 TPH繼承模式后的派生類,在一對一,或者一對零的關系上不支持預先加載模式。這對於 Instructor 實體上的 OfficeAssignment 屬性是個問題。解決這個問題,需要刪除在這個屬性上使用的預先加載處理。
在 InstructorController.cs 文件中,刪除三次出現的如下代碼。
.Include(i => i.OfficeAssignment)
8-7 測試
運行程序,在各個頁面上檢查一下,所有的工作如以前一樣。
在解決方案管理器上,雙擊 School.sdf 數據庫,在服務器資源管理器中打開,展開 School.sdf,然后選擇 Tables,你會看到 Student 和 Instructor 表已經被 Person 表替換掉了。展開 Person 表,你會看到其中擁有原來 Student 和 Instructor 中所有的列,加上 discriminator 列。
下圖展示了新的 School 數據庫的結構。

對於 Person ,Student 和 Instructor 類,通過 TPH 實現了繼承。對於更多的關於其他繼承結構,可以查看 Morteza Manavi 的博客 Inheritance Mapping Strategies。在下一次的教程中,將會學習實現倉儲和單元模式的一些途徑。
