惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
H
Help Net Security
V
Visual Studio Blog
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
The Cloudflare Blog
Martin Fowler
Martin Fowler
D
Docker
腾讯CDC
F
Fortinet All Blogs
雷峰网
雷峰网
GbyAI
GbyAI
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
博客园 - 聂微东
博客园 - 叶小钗

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12)
【EF Core】继承策略——TPT
东邪独孤 · 2026-05-31 · via 博客园_首页

先补充一下前一篇中的 TPH 策略的内容——非完整性类型鉴别器。这个东西官方文档写了等于没写,许多大伙伴可能不知道是啥玩意儿。不用慌,老周给你整个示例,你就懂了。

这种特例多见于先有数据库(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、创建数据库,命名为 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);
// 获取数据集合
DbSet<Student> stus = context.Set<Student>();
// 打印
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();
}

这个代码在运行后,你会看到该错误:

image

现在回过头看看鉴别器配置。

ent.HasDiscriminator<string>("StuType")
    .HasValue<Student>("S")
    .HasValue<TransferStudent>("T")
    .HasValue<RepeatStudent>("R");

再看看数据库中的数据。

select _type from tb_students

image

根据咱们的配置,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")
    .IsComplete(false);

true 表示类型列表是完整的,false 是不完整的。这样配置后就不会抛异常了。

--------------------------------------------------------------------------------------------------------------------------

现在进入主题,今天咱们聊 TPT 策略。TPT 会为每个实体类型独立映射一个数据表,但表中的列仅限于当前类所定义的成员,不包含从基类继承的成员。

咱们依旧使用上面那三个【学生】实体,不过,这次配置为 TPT 映射策略。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 映射策略和主键都要在基类上配置
    modelBuilder.Entity<Student>(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.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 [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();       // 运行时创建数据库
// 获取数据集合
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;

image

【吴珍珠】同学的 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(
    IReadOnlyList<Property>? properties,
    ConfigurationSource configurationSource)
{
    EnsureMutable();
    Check.DebugAssert(IsInModel, "The entity type has been removed from the model");

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

   ……
}

意思就是如果你正在配置的实体存在基类,那就抛出异常。所以,你只能在基类上配置主键。

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);
         }

         name = "PK_" + storeObject.Name;
     }
     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 = entStudent.FindPrimaryKey() as IMutableKey;
            if(key != null)
            {
                key.SetName("PK_Stu_base");
            }
        }
        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.Conventions.Add(_ => new MyConvention());
}

然而,结果会让你失望的。

CREATE TABLE [tb_students] (
    [id] int NOT NULL IDENTITY,
    [name] nvarchar(20) NOT NULL,
    [age] int NOT NULL,
    CONSTRAINT [PK_Rpt_stu] PRIMARY KEY ([id])
);
GO

CREATE TABLE [tb_rpt_students] (
    [mid] int NOT NULL,
    [repeat_grade] int NOT NULL,
    CONSTRAINT [PK_Rpt_stu] 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_Rpt_stu] PRIMARY KEY ([mid]),
    CONSTRAINT [FK_tb_trf_students_tb_students_mid] FOREIGN KEY ([mid]) REFERENCES [tb_students] ([id]) 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 策略下你不能自定义主键的约束名称,但微软说以后的版本会支持。