
这种特例多见于先有数据库DB First的方案。好那咱们就先建库脚本如下很简单。use master; go -- 创建数据库 create database schoolDB; go use schoolDB; go -- 创建表 create table [tb_students] ( -- 基类字段 id int identity not null, [name] nvarchar(20) not null, [age] int not null, -- “转校生”字段 src_school nvarchar(40) null, -- “留级生”字段 repeat_grade int null, -- 鉴别器字段 _type char(1) not null, -- 主键 constraint [PK_Student] primary key ([id] asc) ); go -- 添加点数据 insert into tb_students ([name], age, src_school, repeat_grade, _type) values (N王番薯, 19, NULL, NULL, S), (N吴正经, 20, N华中聊汉大学, NULL, T), (N余小琳, 17, NULL, 3, R), (N欧皮革, 20, NULL, NULL, Z); go上述脚本做了三件事1、创建数据库命名为 schoolDB2、在库中建表名为 tb_students3、往表中写入新数据用于示例。tb_students 表其实包含了三个实体A、正常学生id、name、ageB、转校生在正常学生基础上增加了 src_school 列表示从哪个学校转过来的C、留级生在正常学生基础上增加 repeat_grade 列重读的年级。用作类型鉴别器的是 _type 列S 指代正常学生T 指代转校生R 指代留级生Z 无意义。好了数据库搞好了下面弄 EF Core。先定义三个实体类。/// summary /// 正常学生 /// /summary public class Student { public int Id { get; set; } public required string Name { get; set; } public int Age { get; set; } } /// summary /// 转校生 /// /summary public class TransferStudent : Student { public string SourceSchool { get; set; } null!; } /// summary /// 留级生 /// /summary public class RepeatStudent:Student { public int RepeatGrade { get; set; } }在数据库上下文的 OnModelCreating 方法中配置模型。protected override void OnModelCreating(ModelBuilder modelBuilder) { // 映射策略和主键都要在基类上配置 modelBuilder.EntityStudent(ent { ent.UseTphMappingStrategy(); ent.HasKey(x x.Id); // 表映射 ent.ToTable(tb_students); // 列映射 ent.Property(x x.Id).HasColumnName(id); ent.Property(x x.Name).HasColumnName(name).HasMaxLength(20); ent.Property(x x.Age).HasColumnName(age); // 鉴别器 ent.HasDiscriminatorstring(StuType) .HasValueStudent(S) .HasValueTransferStudent(T) .HasValueRepeatStudent(R); ent.Propertystring(StuType).HasColumnName(_type).HasMaxLength(1); }); // 派生类的映射 modelBuilder.EntityTransferStudent(ent { ent.Property(x x.SourceSchool).HasMaxLength(40).HasColumnName(src_school); }); modelBuilder.EntityRepeatStudent(ent { ent.Property(u u.RepeatGrade).HasColumnName(repeat_grade); }); }现在咱们尝试把所有数据查询出来。// 配置连接字符串 DbContextOptionsBuilderMyContext opbuilder new(); opbuilder.UseSqlServer(Data Source.\\TEST;Initial CatalogschoolDB;Integrated SecurityTrue;Persist Security InfoFalse;EncryptTrue;TrustServerCertificateTrue); using var context new MyContext(opbuilder.Options); // 获取数据集合DbSetStudent stus context.SetStudent(); // 打印 foreach(var s in stus) { Console.WriteLine(id: {0}, s.Id); Console.WriteLine(name: {0}, s.Name); Console.WriteLine(age: {0}, s.Age); if(s is TransferStudent tfstu) { Console.WriteLine(source school: {0}, tfstu.SourceSchool); } if(s is RepeatStudent rpstu) { Console.WriteLine(repeat grade: {0}, rpstu.RepeatGrade); } Console.WriteLine(); }这个代码在运行后你会看到该错误现在回过头看看鉴别器配置。ent.HasDiscriminatorstring(StuType) .HasValueStudent(S) .HasValueTransferStudent(T) .HasValueRepeatStudent(R);再看看数据库中的数据。select _type from tb_students根据咱们的配置Student 类由 S 表示TransferStudent 类由 T 表示RepeatStudent 类由 R 表示。Z 是没有类型映射的这个异常的意思就是类型的鉴别值不完整——就是多了个Z出来EF Core 不知道 Z 跟哪个实体类有关。这种情况我们要明确告诉 EF Core咱们这个数据库中的鉴别器的值与实际的实体类型没有完全匹配的我们所配置的类型鉴别的值是不完整的。ent.HasDiscriminatorstring(StuType) .HasValueStudent(S) .HasValueTransferStudent(T) .HasValueRepeatStudent(R) .IsComplete(false);true 表示类型列表是完整的false 是不完整的。这样配置后就不会抛异常了。--------------------------------------------------------------------------------------------------------------------------现在进入主题今天咱们聊 TPT 策略。TPT 会为每个实体类型独立映射一个数据表但表中的列仅限于当前类所定义的成员不包含从基类继承的成员。咱们依旧使用上面那三个【学生】实体不过这次配置为 TPT 映射策略。protected override void OnModelCreating(ModelBuilder modelBuilder) { // 映射策略和主键都要在基类上配置 modelBuilder.EntityStudent(ent { ent.UseTptMappingStrategy(); ent.HasKey(x x.Id); // 表映射 ent.ToTable(tb_students, tb { tb.Property(u u.Id).HasColumnName(id); tb.Property(u u.Name).HasColumnName(name); tb.Property(u u.Age).HasColumnName(age); }); ent.Property(x x.Name).HasMaxLength(20); }); // 派生类的映射 modelBuilder.EntityTransferStudent(ent { ent.Property(x x.SourceSchool).HasMaxLength(40); // 表映射 ent.ToTable(tb_trf_students, tb { tb.Property(i i.Id).HasColumnName(mid); tb.Property(i i.SourceSchool).HasColumnName(src_school); }); }); modelBuilder.EntityRepeatStudent(ent { // 表映射 ent.ToTable(tb_rpt_students, tb { tb.Property(w w.Id).HasColumnName(mid); tb.Property(w w.RepeatGrade).HasColumnName(repeat_grade); }); }); }不管你用哪种映射策略UseXXXMappingStrategy 方法必须在配置基类实体时调用不能在派生类的配置中调用那样会报错。由于 TPT 是每个类型一个表所以你可以用 ToTable 方法为各个表自定义名称。这里各位要注意列映射的自定义名称最好在 ToTable 方法中通过 TableBuilder 对象来配置不要在实体属性上直接配置ent.Property(...).HasColumnName(...)。这是因为在 PropertyBuilder 上配置的列名是通过 Annotations 字典Key Relational:ColumnName来存储的这表明这个列名你能存储一个值。如果这个属性被多次列映射那么后面设置的列名会覆盖掉前面设置的列名而不管你映射的是否为同一个表。对 TPT 策略而言只有主键列会被多次映射其他属性不会有覆盖的问题派生类的表不包含基类成员自然就不会重复映射了。比如基类 Student在 tb_students 表中映射了 Id、Name、Age 属性到了 TransferStudent 类它只定义了 SourceSchool 属性所以表 tb_trf_students 中只映射 SourceSchool 成员。RepeatStudent 实体同理。从上面的配置代码看到只有 Id 属性被做了多次列映射。所以除了 Id 属性以外其他属性是可以在 PropertyBuilder 上用 HasColumnName 方法配置列映射的但为了代码更好看统一用 TableBuilder 来配置最好。尤其在 TPC 策略下各个属性都会多次映射本文先不提。那么为什么 TPT 策略要把基类的主键映射多次呢看看它生成的 SQL 语句你或许就明白了。CREATE TABLE [tb_students] ( [id] int NOT NULL IDENTITY, [name] nvarchar(20) NOT NULL, [age] int NOT NULL, CONSTRAINT[PK_tb_students] PRIMARY KEY ([id])); GO CREATE TABLE [tb_rpt_students] ( [mid] int NOT NULL, [repeat_grade] int NOT NULL, CONSTRAINT [PK_tb_rpt_students] PRIMARY KEY ([mid]), CONSTRAINT[FK_tb_rpt_students_tb_students_mid] FOREIGN KEY ([mid]) REFERENCES [tb_students] ([id])ON DELETE CASCADE ); GO CREATE TABLE [tb_trf_students] ( [mid] int NOT NULL, [src_school] nvarchar(40) NOT NULL, CONSTRAINT [PK_tb_trf_students] PRIMARY KEY ([mid]), CONSTRAINT[FK_tb_trf_students_tb_students_mid] FOREIGN KEY ([mid]) REFERENCES [tb_students] ([id])ON DELETE CASCADE ); GO不知道大伙伴们看出啥门道了没有。在 TPT 映射策略中只有基类的主键列会生成/插入新值其他派生类表都是通过外键来引用基类表的主键的。正因为这样所以在查询数据时就等于做联表查询这使得 TPT 策略的性能会比其他策略低。啥意思呢咱们试着插入几条记录就知道了。using var context new MyContext(opbuilder.Options); context.Database.EnsureCreated(); // 运行时创建数据库 // 获取数据集合 DbSetStudent students context.SetStudent(); // 添加新记录 students.AddRange([ new Student{Name 吴珍珠, Age 18}, new TransferStudent{Name 王大山, Age 18, SourceSchool 飓风中学}, new RepeatStudent{Name 陆大锤, Age 17, RepeatGrade 2} ]); // 保存数据 context.SaveChanges();咱们每个类型各添加一条记录看看数据库怎么存储它们。select * from tb_students; select * from tb_trf_students; select * from tb_rpt_students;【吴珍珠】同学的 Id 为2因为它是 Student 类作为基类只用到 tb_students 表【王大山】同学的 Id 为 3它是 TransferStudent 类。从基类继承的 Name 和 Age 属性存放到 tb_students 表中而 SourceSchool 属性的值则存放在 tb_trf_students 表的 src_school 列中【陆大锤】同学的 Id 为1它是 RepeatStudent 类其中 Name、Age 属性存入 tb_students 列而它所定义的 RepeatGrade 属性的值就存入 tb_rpt_students 表的 repeat_grade 列。最后咱们把注意力放在主键列上。所有记录的主键值都在基类表中生成tb_students.id 列然后对于【吴珍珠】同学它就在基类表中不需要外键引用对于【王大山】同学tb_trf_students.mid 列通过外键引用了主键值 3对于【陆大锤】同学tb_rpt_students.mid 列通过外键引用了主键值 1目前 EF Core 在配置主键的约束名称是有限制的所以不要去自定义主键的约束。// 不要调用 HasName 方法 ent.HasKey(x x.Id).HasName(PK_what_the_fk);下面老周解释一下为什么会有这个局限。1、派生类中不允许配置主键。看看 EntityType.SetPrimaryKey 方法的源代码。public virtual Key? SetPrimaryKey( IReadOnlyListProperty? properties, ConfigurationSource configurationSource) { EnsureMutable(); Check.DebugAssert(IsInModel, The entity type has been removed from the model);if (BaseType ! null)throw newInvalidOperationException(CoreStrings.DerivedEntityTypeKey(DisplayName(), GetRootType().DisplayName()));}