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

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
Netflix TechBlog - Medium
The Register - Security
The Register - Security
aimingoo的专栏
aimingoo的专栏
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园 - 司徒正美
The Cloudflare Blog
GbyAI
GbyAI
IT之家
IT之家
A
About on SuperTechFans
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
U
Unit 42
美团技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
F
Full Disclosure
B
Blog RSS Feed
F
Fortinet All Blogs
MongoDB | Blog
MongoDB | Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
WordPress大学
WordPress大学
J
Java Code Geeks
G
Google Developers Blog
Jina AI
Jina AI
量子位
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
有赞技术团队
有赞技术团队
L
LangChain Blog
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
H
Help Net Security
月光博客
月光博客
雷峰网
雷峰网
D
Docker
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
博客园 - 【当耐特】
Stack Overflow Blog
Stack Overflow Blog
Apple Machine Learning Research
Apple Machine Learning Research
Y
Y Combinator Blog
小众软件
小众软件
博客园 - 聂微东
爱范儿
爱范儿
D
DataBreaches.Net

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
【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 策略下你不能自定义主键的约束名称,但微软说以后的版本会支持。