上一章你学习了继承,本章中你将学习如何在数据模型中实现继承。
在“面向对象”的编程中你通常使用“继承”避免冗余代码,本章中你将让Instructor和Student两个模型类分别继承基本类Person,该类包含基本的共有属性(比如LastName等)。对于视图页面您无需做任何更改,不过你可能需要稍微更改一些代码,并且这些改变会在数据库中反映出来。
【树形表结构VS继承表结构】
在“面向对象”编程中,你使用继承使得相关类编码变得更为简单。举例来说——Instructor和Student两个模型类共享一些属性,结果导致了冗余代码的产生:
假如你想消除由于Instructor和Student共享属性而带来的代码冗余,那么你应该创建一个具备这些共享属性的基本类Person,然后让Instructor和Student都继承自它,如下所示:
“继承结构”在数据表中的表现形式可有几种情况:你可以创建一个包含Instructor和Student信息的类Person,一些属性只针对Instructor有效(比如HireDate),另一些属性则只针对Student有效(比如EnrollmentDate),而另一些是它们所共有的(比如LastName和FirstName);通常在这种情况下你应该有一个“鉴别列”(discriminator)用于鉴别哪行属于哪个类型(本示例中,discriminator列拥有“Instructor”表示属于Instructor,拥有Student则隶属于Student)。
以上“直接从一个数据表”生成的实体模型结构被称为“树形继承结构”(TPH)。
另外一种方法使得数据表看上去更像“继承”结构——比如在Person模型中只有Name相关的信息,而在Instructor和Student表中则仅存储了日期相关的信息。
以上为每个实体结构创建的数据表的形式被成为“单类型表”继承(TPT)。
TPH在EntityFramework中通常而言性能好于TPT,因为TPT可能导致复杂的表连接查询。本章主要介绍如何实现TPH继承。你将通过以下步骤实现该目的:
1)创建一个Person类,并且使得Instructor和Student继承自该模型类。
2)在数据上下文类中添加关于“模型=>数据表”的转换代码。
3)把整个项目中的所有InstructorId和StudentId改成PersonId。
【创建Person类】
在“Models”文件夹中,创建一个Person类,用以下代码替换自生成代码:
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类,删除其中的主键和其它字段,代码如下:
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; } } }
对Students类也做如上的修正——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; } } }
【在模型中添加Person实体】
在SchoolContext文件中,为Person添加一个DbSet属性:
public DbSet<Person> People { get; set; }
以上就是EntityFramework所需要的一切,就此也完成了对于“树形结构”的配置。如你可以看到的一样——当数据库创建之时,只创建了Person数据表,却没有创建Instructor或Student数据表。
【把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的),改为PersonID,同样把StudentId也改成PersonId;注意“大小写”敏感问题(此凸显了对于主键更名——“类名+Id”形式的缺陷:如果你对主键重命名,但是该重命名不带有“类名”前缀的话,现在就没有必要重命名了)。
【在Initializer中修正主键数值】
在“SchoolInitializer.cs”中,代码假设对于Student或Instructor而言主键都是分别编号的。此对于Student仍旧是正确的(因为仍然从1~8)。但是对于Instructor而言是从9~13,并非1~5;因为在初始化中动态添加课程的代码段紧挨着添加学生的代码段之后,所以请用以下使用新Id的代码替换原先产生Department和OfficeAssignment实体集:
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" }, };
【把OfficeAssignment改成“慢模式”加载】
当前的EntityFramework版本并不支持处于继承类TPH导航属性“一对一(零)”关系的“饥饿模式”,因此这也就是在Instructor实体上的OfficeAssignment导航属性的情况——为了解决此问题,你应该移除原先在这个属性上使用“饥饿模式”加载的代码(在“InstructorController.cs”中删除三处这样的代码):
.Include(i => i.OfficeAssignment)
【测试】
运行网站项目,尝试在不同页切换功能,结果发现和原先一致:
在Solution Explorer中双击打开School.sdf以便打开该数据库,展开此库和表,你发现Instructor和Student表消失不见,取而代之的只有Person表;展开此表你会看到包含所有Instructor和Student列字段,同时还包含一个discriminator字段:
以下表关系展示了新设计的School数据库的结构:
通过实现Instructor,Student以及Person类,TPH形式目前已经得以实现;有关TPH更多的信息以及其它设计模式你可以参考 Morteza Manavi的博客(Inheritance Mapping Strategies)。下一节你要学习一些方法去实现数据存储以及单元工作。
关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。