Entity Framework应用:Code First的实体继承模式
Entity Framework的Code First模式有三种实体继承模式
1、Table per Type (TPT)继承
2、Table per Class Hierarchy(TPH)继承
3、Table per Concrete Class (TPC)继承
一、TPT继承模式
当领域实体类有继承关系时,TPT继承很有用,我们想把这些实体类模型持久化到数据库中,这样,每个领域实体都会映射到单独的一张表中。这些表会使用一对一关系相互关联,数据库会通过一个共享的主键维护这个关系。
假设有这么一个场景:一个组织维护了一个部门工作的所有人的数据库,这些人有些是拿着固定工资的员工,有些是按小时付费的临时工,要持久化这个场景,我们要创建三个领域实体:Person、Employee和Vendor类。Person类是基类,另外两个类会继承自Person类。实体类结构如下:
1、Person类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace TPTPattern.Model 8 { 9 public class Person 10 { 11 public int Id { get; set; } 12 13 public string Name { get; set; } 14 15 public string Email { get; set; } 16 17 public string PhoneNumber { get; set; } 18 } 19 }
Employee类结构
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations.Schema; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace TPTPattern.Model 9 { 10 [Table("Employee")] 11 public class Employee :Person 12 { 13 /// <summary> 14 /// 薪水 15 /// </summary> 16 public decimal Salary { get; set; } 17 } 18 }
Vendor类结构
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations.Schema; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace TPTPattern.Model 9 { 10 [Table("Vendor")] 11 public class Vendor :Person 12 { 13 /// <summary> 14 /// 每小时的薪水 15 /// </summary> 16 public decimal HourlyRate { get; set; } 17 } 18 }
在VS中的类图如下:
对于Person类,我们使用EF的默认约定来映射到数据库,而对Employee和Vendor类,我们使用了数据注解,将它们映射为我们想要的表名。
然后我们需要创建自己的数据库上下文类,数据库上下文类定义如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using TPTPattern.Model; 8 9 namespace TPTPattern.EFDatabaseContext 10 { 11 public class EFDbContext :DbContext 12 { 13 public EFDbContext() 14 : base("name=Default") 15 { } 16 17 public DbSet<Person> Persons { get; set; } 18 } 19 }
在上面的上下文中,我们只添加了实体类Person的DbSet,没有添加另外两个实体类的DbSet。因为其它的两个领域模型都是从这个模型派生的,所以我们也就相当于将其它两个类添加到了DbSet集合中了,这样EF会使用多态性来使用实际的领域模型。当然,也可以使用Fluent API和实体伙伴类来配置映射细节信息。
2、使用数据迁移创建数据库
使用数据迁移创建数据库后查看数据库表结构:
在TPT继承中,我们想为每个领域实体类创建单独的一张表,这些表共享一个主键。因此生成的数据库关系图表如下:
3、填充数据
现在我们使用这些领域实体来创建一个Employee和Vendor类来填充数据,Program类定义如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using TPTPattern.EFDatabaseContext; 7 using TPTPattern.Model; 8 9 namespace TPTPattern 10 { 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 using (var context = new EFDbContext()) 16 { 17 Employee emp = new Employee() 18 { 19 Name="李白", 20 Email="LiBai@163.com", 21 PhoneNumber="18754145782", 22 Salary=2345m 23 }; 24 25 Vendor vendor = new Vendor() 26 { 27 Name="杜甫", 28 Email="DuFu@qq.com", 29 PhoneNumber="18234568123", 30 HourlyRate=456m 31 }; 32 33 context.Persons.Add(emp); 34 context.Persons.Add(vendor); 35 context.SaveChanges(); 36 } 37 38 Console.WriteLine("信息录入成功"); 39 } 40 } 41 }
查询数据库填充后的数据:
我们可以看到每个表都包含单独的数据,这些表之间都有一个共享的主键。因而这些表之间都是一对一的关系。
注:TPT模式主要应用在一对一模式下。
二、TPH模式
当领域实体有继承关系时,但是我们想将来自所有的实体类的数据保存到单独的一张表中时,TPH继承很有用。从领域实体的角度,我们的模型类的继承关系仍然像上面的截图一样:
但是从数据库的角度讲,应该只有一张表保存数据。因此,最终生成的数据库的样子应该是下面这样的:
注意:从数据库的角度看,这种模式很不优雅,因为我们将无关的数据保存到了单张表中,我们的表是不标准的。如果我们使用这种方法,那么总会存在null值的冗余列。
1、创建有继承关系的实体类
现在我们创建实体类来实现该继承,注意:这次创建的三个实体类和之前创建的只是没有了类上面的数据注解,这样它们就会映射到数据库的单张表中(EF会默认使用父类的DbSet属性名或复数形式作为表名,并且将派生类的属性映射到那张表中),类结构如下:
2、创建数据上下文
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using TPHPattern.Model; 8 9 namespace TPHPattern.EFDatabaseContext 10 { 11 public class EFDbContext :DbContext 12 { 13 public EFDbContext() 14 : base("name=Default") 15 { } 16 17 public DbSet<Person> Persons { get; set; } 18 19 public DbSet<Employee> Employees { get; set; } 20 21 public DbSet<Vendor> Vendors { get; set; } 22 } 23 }
3、使用数据迁移创建数据库
使用数据迁移生成数据库以后,会发现数据库中只有一张表,而且三个实体类中的字段都在这张表中了, 创建后的数据库表结构如下:
注意:查看生成的表结构,会发现生成的表中多了一个Discriminator字段,它是用来找到记录的实际类型,即从Person表中找到Employee或者Vendor。
4、不使用默认生成的区别多张表的类型
使用Fluent API,修改数据上下文类,修改后的类定义如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using TPHPattern.Model; 8 9 namespace TPHPattern.EFDatabaseContext 10 { 11 public class EFDbContext :DbContext 12 { 13 public EFDbContext() 14 : base("name=Default") 15 { } 16 17 public DbSet<Person> Persons { get; set; } 18 19 public DbSet<Employee> Employees { get; set; } 20 21 public DbSet<Vendor> Vendors { get; set; } 22 23 protected override void OnModelCreating(DbModelBuilder modelBuilder) 24 { 25 // 强制指定PersonType是鉴别器 1代表全职职员 2代表临时工 26 modelBuilder.Entity<Person>() 27 .Map<Employee>(m => m.Requires("PersonType").HasValue(1)) 28 .Map<Vendor>(m => m.Requires("PersonType").HasValue(2)); 29 base.OnModelCreating(modelBuilder); 30 } 31 } 32 }
重新使用数据迁移把实体持久化到数据库,持久化以后的数据库表结构:
生成的PersonType列的类型是int。
5、填充数据
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using TPHPattern.EFDatabaseContext; 7 using TPHPattern.Model; 8 9 namespace TPHPattern 10 { 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 using (var context = new EFDbContext()) 16 { 17 Employee emp = new Employee() 18 { 19 Name = "李白", 20 Email = "LiBai@163.com", 21 PhoneNumber = "18754145782", 22 Salary = 2345m 23 }; 24 25 Vendor vendor = new Vendor() 26 { 27 Name = "杜甫", 28 Email = "DuFu@qq.com", 29 PhoneNumber = "18234568123", 30 HourlyRate = 456m 31 }; 32 33 context.Persons.Add(emp); 34 context.Persons.Add(vendor); 35 context.SaveChanges(); 36 } 37 38 Console.WriteLine("信息录入成功"); 39 } 40 } 41 }
6、查询数据
注意:TPH模式和TPT模式相比,TPH模式只是少了使用数据注解或者Fluent API配置子类的表名。因此,如果我们没有在具有继承关系的实体之间提供确切的配置,那么EF会默认将其对待成TPH模式,并把数据放到单张表中。
三、TPC模式
当多个领域实体类派生自一个基类实体,并且我们想将所有具体类的数据分别保存在各自的表中,以及抽象基类实体在数据库中没有对应的表时,使用TPC继承模式。实体模型还是和之前的一样。
然而,从数据库的角度看,只有所有具体类所对应的表,而没有抽象类对应的表。生成的数据库如下图:
1、创建实体类
创建领域实体类,这里Person基类应该是抽象的,其他的地方都和上面一样:
2、配置数据上下文
接下来就是应该配置数据库上下文了,如果我们只在数据库上下文中添加了Person的DbSet泛型属性集合,那么EF会当作TPH继承处理,如果我们需要实现TPC继承,那么还需要使用Fluent API来配置映射(当然也可以使用配置伙伴类),数据库上下文类定义如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using TPCPattern.Model; 8 9 namespace TPCPattern.EFDatabaseContext 10 { 11 public class EFDbContext :DbContext 12 { 13 public EFDbContext() 14 : base("name=Default") 15 { } 16 17 public virtual DbSet<Person> Persons { get; set; } 18 19 protected override void OnModelCreating(DbModelBuilder modelBuilder) 20 { 21 //MapInheritedProperties表示继承以上所有的属性 22 modelBuilder.Entity<Employee>().Map(m => 23 { 24 m.MapInheritedProperties(); 25 m.ToTable("Employees"); 26 }); 27 modelBuilder.Entity<Vendor>().Map(m => 28 { 29 m.MapInheritedProperties(); 30 m.ToTable("Vendors"); 31 }); 32 base.OnModelCreating(modelBuilder); 33 } 34 } 35 }
上面的代码中,MapInheritedProperties方法将继承的属性映射到表中,然后我们根据不同的对象类型映射到不同的表中。
3、使用数据迁移生成数据库
生成的数据库表结构如下:
查看生成的表结构会发现,生成的数据库中只有具体类对应的表,而没有抽象基类对应的表。具体实体类对应的表中有所有抽象基类里面的字段。
4、填充数据
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using TPCPattern.EFDatabaseContext; 7 using TPCPattern.Model; 8 9 namespace TPCPattern 10 { 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 using (var context = new EFDbContext()) 16 { 17 Employee emp = new Employee() 18 { 19 Name = "李白", 20 Email = "LiBai@163.com", 21 PhoneNumber = "18754145782", 22 Salary = 2345m 23 }; 24 25 Vendor vendor = new Vendor() 26 { 27 Name = "杜甫", 28 Email = "DuFu@qq.com", 29 PhoneNumber = "18234568123", 30 HourlyRate = 456m 31 }; 32 33 context.Persons.Add(emp); 34 context.Persons.Add(vendor); 35 context.SaveChanges(); 36 } 37 38 Console.WriteLine("信息录入成功"); 39 } 40 } 41 }
查询数据库:
注意:虽然数据是插入到数据库了,但是运行程序时也出现了异常,异常信息见下图。出现该异常的原因是EF尝试去访问抽象类中的值,它会找到两个具有相同Id的记录,然而Id列被识别为主键,因而具有相同主键的两条记录就会产生问题。这个异常清楚地表明了存储或者数据库生成的Id列对TPC继承无效。
如果我们想使用TPC继承,那么要么使用基于GUID的Id,要么从应用程序中传入Id,或者使用能够维护对多张表自动生成的列的唯一性的某些数据库机制。