不到断气不罢休 发表于 3 天前

【EF Core】继承战略——TPT

先增补一下前一篇中的 TPH 战略的内容——非完备性范例辨别器。这个东西官方文档写了便是没写,很多大同伴大概不知道是啥玩意儿。不消慌,老周给你整个示例,你就懂了。
这种特例多见于先有数据库(DB First)的方案。好,那咱们就先建库,脚本如下,很简单。
use master;
go

-- 创建数据库
create database schoolDB;
go

use schoolDB;
go

-- 创建表
create table
(
    -- 基类字段
    id int identity not null,
    nvarchar(20) not null,
    int not null,
    -- “转校生”字段
    src_school nvarchar(40) null,
    -- “留级生”字段
    repeat_grade int null,
    -- 鉴别器字段
    _type char(1) not null,
    -- 主键
    constraint primary key ( asc)
);
go

-- 添加点数据
insert into tb_students
    (, 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、创建数据库,定名为 schoolDB;
2、在库中建表,名为 tb_students;
3、往表中写入新数据,用于示例。
tb_students 表着实包罗了三个实体:
A、正常门生(id、name、age);
B、转校生,在正常门生根本上增长了 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.Entity<Student>(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.HasDiscriminator<string>("StuType")
            .HasValue<Student>("S")
            .HasValue<TransferStudent>("T")
            .HasValue<RepeatStudent>("R");
      ent.Property<string>("StuType").HasColumnName("_type").HasMaxLength(1);
    });

    // 派生类的映射
    modelBuilder.Entity<TransferStudent>(ent =>
    {
      ent.Property(x => x.SourceSchool).HasMaxLength(40).HasColumnName("src_school");
    });
    modelBuilder.Entity<RepeatStudent>(ent =>
    {
      ent.Property(u => u.RepeatGrade).HasColumnName("repeat_grade");
    });
}如今咱们实验把全部数据查询出来。
// 配置连接字符串
DbContextOptionsBuilder<MyContext> opbuilder = new();
opbuilder.UseSqlServer("Data Source=.\\TEST;Initial Catalog=schoolDB;Integrated Security=True;Persist Security Info=False;Encrypt=True;TrustServerCertificate=True");

using var context = new MyContext(opbuilder.Options);
// 获取数据集合
<strong>DbSet<Student> stus = context.Set<Student></strong><strong>()</strong>;
// 打印
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();
}这个代码在运行后,你会看到该错误:
https://img2024.cnblogs.com/blog/367389/202605/367389-20260531111207073-1232395123.png
如今回过头看看辨别器设置。
ent.HasDiscriminator<string>("StuType")
    .HasValue<Student>("S")
    .HasValue<TransferStudent>("T")
    .HasValue<RepeatStudent>("R");再看看数据库中的数据。
select _type from tb_studentshttps://img2024.cnblogs.com/blog/367389/202605/367389-20260531111436244-1113667300.png
根据咱们的设置,Student 类由 S 表现,TransferStudent 类由 T 表现,RepeatStudent 类由 R 表现。Z 是没有范例映射的,这个非常的意思就是范例的辨别值不完备——就是多了个Z出来,EF Core 不知道 Z 跟哪个实体类有关。
这种情况,我们要明白告诉 EF Core,咱们这个数据库中的辨别器的值与现实的实体范例没有完全匹配的,我们所设置的范例辨别的值是不完备的。
ent.HasDiscriminator<string>("StuType")
    .HasValue<Student>("S")
    .HasValue<TransferStudent>("T")
    .HasValue<RepeatStudent>("R")
    .<strong>IsComplete(</strong><strong>false)</strong>;true 表现范例列表是完备的,false 是不完备的。如许设置后就不会抛非常了。
--------------------------------------------------------------------------------------------------------------------------
如今进入主题,本日咱们聊 TPT 战略。TPT 会为每个实体范例独立映射一个数据表,但表中的列仅限于当前类所界说的成员,不包罗从基类继承的成员。
咱们仍旧利用上面那三个【门生】实体,不外,这次设置为 TPT 映射战略。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 映射策略和主键都要在基类上配置
    modelBuilder.Entity<Student>(ent =>
    {
      ent.<strong>UseTptMappingStrategy()</strong>;
      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.Entity<TransferStudent>(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.Entity<RepeatStudent>(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 (
    int NOT NULL IDENTITY,
    nvarchar(20) NOT NULL,
    int NOT NULL,
    CONSTRAINT <strong> PRIMARY KEY (</strong><strong>)</strong>
);
GO

CREATE TABLE (
    int NOT NULL,
    int NOT NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT <strong> FOREIGN KEY () REFERENCES ()</strong> ON DELETE CASCADE
);
GO

CREATE TABLE (
    int NOT NULL,
    nvarchar(40) NOT NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT <strong> FOREIGN KEY () REFERENCES ()</strong> ON DELETE CASCADE
);
GO不知道大同伴们看出啥门道了没有。在 TPT 映射战略中,只有基类的主键列会天生/插入新值,其他派生类表都是通过外键来引用基类表的主键的。正由于如许,以是在查询数据时,就便是做联表查询,这使得 TPT 战略的性能会比其他战略低。
 啥意思呢,咱们试着插入几条纪录就知道了。
using var context = new MyContext(opbuilder.Options);

context.Database.EnsureCreated();       // 运行时创建数据库
// 获取数据集合
DbSet<Student> students = context.Set<Student>();
// 添加新记录
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;https://img2024.cnblogs.com/blog/367389/202605/367389-20260531121534669-944670084.png
【吴珍珠】同砚的 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).<strong>HasName</strong>("PK_what_the_fk");下面老周表明一下为什么会有这个范围。
1、派生类中不答应设置主键。看看 EntityType.SetPrimaryKey 方法的源代码。
public virtual Key? SetPrimaryKey(
    IReadOnlyList<Property>? properties,
    ConfigurationSource configurationSource)
{
    EnsureMutable();
    Check.DebugAssert(IsInModel, "The entity type has been removed from the model");

    <strong>if (BaseType != null)
</strong>    <strong>throw new</strong><strong> InvalidOperationException(CoreStrings.DerivedEntityTypeKey(DisplayName(), GetRootType().DisplayName()));
</strong>    <strong>}</strong>

   ……
}意思就是假如你正在设置的实体存在基类,那就抛出非常。以是,你只能在基类上设置主键。
2、对于 EF Core 的数据库模子,假如实体存在继承关系,那么,派生类实体所继承的成员,与基类实体所界说的同一个成员,它们之间利用雷同的元数据。这结果是,假如你在 Student 类中设置了主键的束缚名为 PK_XXX,那么,TransferStudent 类和 RepeatStudent 类的 Id 属性都从 Student 害继承,即它们的元数据雷同,导致全部数据表的主键的束缚名都酿成 PK_XXX。多个表利用雷同的束缚名,在数据库中会报错。
以是,你不能改变束缚名,一改就全部一起改掉了。但保存 EF Core 的默认设置就没有题目,由于 EF Core 在天生 SQL 语句时,主键默认的名字是 PK_,外键是 FK_,如许就不会出现重复束缚名了。
public static string? GetDefaultName(
   this IReadOnlyKey key,
   in StoreObjectIdentifier storeObject,
   IDiagnosticsLogger<DbLoggerCategory.Model.Validation>? logger)
{
   if (storeObject.StoreObjectType != StoreObjectType.Table
         || key.DeclaringEntityType.IsMappedToJson())
   {
         return null;
   }

   if (key.DeclaringEntityType.IsMappedToJson())
   {
         return null;
   }

   string? name;
   if (key.IsPrimaryKey())
   {
         var rootKey = key;
         // Limit traversal to avoid getting stuck in a cycle (validation will throw for these later)
         // Using a hashset is detrimental to the perf when there are no cycles
         for (var i = 0; i < RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++)
         {
             var linkingFk = rootKey!.DeclaringEntityType.FindRowInternalForeignKeys(storeObject)
               .FirstOrDefault();
             if (linkingFk == null)
             {
               break;
             }

             rootKey = linkingFk.PrincipalEntityType.FindPrimaryKey();
         }

         if (rootKey != null
             && rootKey != key)
         {
             return rootKey.GetName(storeObject);
         }

         <strong>name </strong><strong>= "PK_" +</strong><strong> storeObject.Name;</strong>
   }
   else
   {
         var columnNames = key.Properties.GetColumnNames(storeObject);
         if (columnNames == null)
         {
             if (logger != null)
             {
               var table = storeObject;
               if (key.DeclaringEntityType.GetMappingFragments(StoreObjectType.Table)
                     .Any(t => t.StoreObject != table && key.Properties.GetColumnNames(t.StoreObject) != null))
               {
                     return null;
               }

               if (key.DeclaringEntityType.GetMappingStrategy() != RelationalAnnotationNames.TphMappingStrategy
                     && key.DeclaringEntityType.GetDerivedTypes()
                         .Select(e => StoreObjectIdentifier.Create(e, StoreObjectType.Table))
                         .Any(t => t != null && key.Properties.GetColumnNames(t.Value) != null))
               {
                     return null;
               }

               logger.KeyPropertiesNotMappedToTable((IKey)key);
             }

             return null;
         }

         var rootKey = key;

         // Limit traversal to avoid getting stuck in a cycle (validation will throw for these later)
         // Using a hashset is detrimental to the perf when there are no cycles
         for (var i = 0; i < RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++)
         {
             IReadOnlyKey? linkedKey = null;
             foreach (var otherKey in rootKey.DeclaringEntityType
                        .FindRowInternalForeignKeys(storeObject)
                        .SelectMany(fk => fk.PrincipalEntityType.GetKeys()))
             {
               var otherColumnNames = otherKey.Properties.GetColumnNames(storeObject);
               if ((otherColumnNames != null)
                     && otherColumnNames.SequenceEqual(columnNames))
               {
                     linkedKey = otherKey;
                     break;
               }
             }

             if (linkedKey == null)
             {
               break;
             }

             rootKey = linkedKey;
         }

         if (rootKey != key)
         {
             return rootKey.GetName(storeObject);
         }

         name = new StringBuilder()
             .Append("AK_")
             .Append(storeObject.Name)
             .Append('_')
             .AppendJoin(columnNames, "_")
             .ToString();
   }

   return Uniquifier.Truncate(name, key.DeclaringEntityType.Model.GetMaxIdentifierLength());

这时间有大同伴大概想到了利用约定来修改主键的束缚名称。
public class MyConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
      var entStudent = modelBuilder.Metadata.FindEntityType(typeof(Student));
      if(entStudent != null)
      {
            var key = <strong>entStudent.FindPrimaryKey() as</strong><strong> IMutableKey</strong>;
            if(key != null)
            {
                <strong>key.SetName(</strong><strong>"PK_Stu_base"</strong><strong>)</strong>;
            }
      }
      var entTrfStudent = modelBuilder.Metadata.FindEntityType(typeof(TransferStudent));
      if(entTrfStudent != null)
      {
            var key = entTrfStudent.FindPrimaryKey() as IMutableKey;
            if(key != null)
            {
                key.SetName("PK_Transf_stu");
            }
      }
      var entRptStudent = modelBuilder.Metadata.FindEntityType(typeof(RepeatStudent));
      if (entRptStudent != null)
      {
            var key = entRptStudent.FindPrimaryKey() as IMutableKey;
            if (key != null)
            {
                key.SetName("PK_Rpt_stu");
            }
      }
    }
}数据库模子一旦 Finalized 阶段就酿成只读了,无法修改元数据。以是你不能在实例化 DbContext 之后修改,当时间已经改不了。故,咱们要用约定的话,只能在 Finalizing 阶段。这时间模子的设置已经完成,但还未被固化(只读),即实现 IModelFinalizingConvention 接口,如许做可以制止被其他约定干扰。
约定类写好后,重写 DbContext.ConfigureConventions 方法,将其注册到约定聚集中。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.<strong>Conventions.Add(_ </strong><strong>=> new</strong><strong> MyConvention())</strong>;
}然而,结果会让你扫兴的。
CREATE TABLE (
    int NOT NULL IDENTITY,
    nvarchar(20) NOT NULL,
    int NOT NULL,
    CONSTRAINT PRIMARY KEY ()
);
GO

CREATE TABLE (
    int NOT NULL,
    int NOT NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT FOREIGN KEY () REFERENCES () ON DELETE CASCADE
);
GO

CREATE TABLE (
    int NOT NULL,
    nvarchar(40) NOT NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT FOREIGN KEY () REFERENCES () ON DELETE CASCADE
);
GO只要改此中一个,便是全部主键都改了。这时可以开端推断,由于主键是从基类继承的,以是,派生类实体的元数据中,利用的主键对象是同一个实例。
要证明这个推测也很容易,我们打印出三个实体的 Key 对象的内存地点。
public class MyConvention : IModelFinalizingConvention
{
    private void PrintObjectAddress(string tag, object obj)
    {
      GCHandle handle = GCHandle.Alloc(obj, GCHandleType.WeakTrackResurrection);
      IntPtr addr = GCHandle.ToIntPtr(handle);
      handle.Free();
      Console.WriteLine("{0}: 0x{1:X}", tag, addr);
    }

    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
      var entStudent = modelBuilder.Metadata.FindEntityType(typeof(Student));
      if(entStudent != null)
      {
            var key = entStudent.FindPrimaryKey() as IMutableKey;
            if(key != null)
            {
                key.SetName("PK_Stu_base");
                PrintObjectAddress("Student Key", key);
            }
      }
      var entTrfStudent = modelBuilder.Metadata.FindEntityType(typeof(TransferStudent));
      if(entTrfStudent != null)
      {
            var key = entTrfStudent.FindPrimaryKey() as IMutableKey;
            if(key != null)
            {
                key.SetName("PK_Transf_stu");
                PrintObjectAddress("TransferStudent Key", key);
            }
      }
      var entRptStudent = modelBuilder.Metadata.FindEntityType(typeof(RepeatStudent));
      if (entRptStudent != null)
      {
            var key = entRptStudent.FindPrimaryKey() as IMutableKey;
            if (key != null)
            {
                key.SetName("PK_Rpt_stu");
                PrintObjectAddress("RepeatStudent Key", key);
            }
      }
    }
}然后得到以下结果:
Student Key:            0x245CD862820
TransferStudent Key:      0x245CD862820
RepeatStudent Key:      0x245CD862820            看吧,它们的地点一样。
以是,现阶段,在 TPT 战略下你不能自界说主键的束缚名称,但微软说以后的版本会支持。
 

免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.
页: [1]
查看完整版本: 【EF Core】继承战略——TPT