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

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
K
Kaspersky official blog
A
Arctic Wolf
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
W
WeLiveSecurity
Know Your Adversary
Know Your Adversary
博客园 - Franky
T
Tenable Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Help Net Security
Help Net Security
WordPress大学
WordPress大学
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
S
Security Affairs
J
Java Code Geeks
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Apple Machine Learning Research
Apple Machine Learning Research
NISL@THU
NISL@THU
O
OpenAI News
The Cloudflare Blog
月光博客
月光博客
Google Online Security Blog
Google Online Security Blog
V
V2EX
罗磊的独立博客
美团技术团队
博客园 - 三生石上(FineUI控件)
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏

博客园_首页

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】继承策略——TPH
东邪独孤 · 2026-05-17 · via 博客园_首页

既然 EF Core 的设计理念是以面向对象的方式操作数据库,那么,继承问题是绕不过去的。然而大伙伴也知道,数据库是用表来存储数据记录的,表与表之间哪来的继承。很自然地,EF Core 必须在数据库与实体类之间做一些特殊处理,才能让存在继承关系的实体类与数据表之间的映射不被破坏。

EF Core 内置了三种应对策略:

1、TPH:全称 Table Per Hierarchy。其特点是将整个继承链上的实体都存放在一个数据表中。假设有 X、Y 两实体,Y 继承 X,于是,两个实体都存到名为 T_abc 的表中。由于多个类型映射到一个表中,为了区分,EF Core 会向数据表添加一个额外的列,用于标识类型。默认用的是实体类的名称。

2、TPT:Table-Per-Type。从命名可以看出,每个类型对应一个表。假设 B 继承 A,那么,A 映射到 TA 表,B 映射到 TB 表。但,数据表只映射当前类所定义的成员,不包括从基类继承的成员。假如,A 类有 ID、X 两个属性,那么,TA 表就会映射 ID 和 X;B 类定义了 C 属性,映射时只把 C 属性映射到 TB 表,不会映射从 A 类继承的 ID 和 X 属性。

3、TPC:即 Table-Per-Concrete-Type。它强调“具体类型”,其实它和 TPT 很像,都每个实体都映射一个表,但是,TPC 策略中,派生类会把从基类继承的成员也映射到表中。引沿上面举的例子,即 A 类会映射 ID 和 X 属性到 TA 表,而 B 类会映射 ID、X、C 属性到 TB 表。

咱们先来研究 TPH 策略。

我们举个例子,有两个类,它们存在继承关系。

// 基类
public class BaseType
{
    public int Bid { get; set; }
}

// 派生类
public class DeriveType : BaseType
{
    public double SValue { get; set; }
}

EF Core 的公共约定类具备自动发现继承链的能力,前提是你得先把这些实体类添加到数据库模型中。因此,重写 DbContext 类的 OnModelCreating 方法时,代码可以这样写:

// 配置基类实体
modelBuilder.Entity<BaseType>(ent =>
{
    ent.HasKey(x => x.Bid);     // 主键
    ent.ToTable("tb_zbzb");     // 设置表名
});
// 配置派生类实体
modelBuilder.Entity<DeriveType>();

在配置模型时,有些地方得注意:

A、TPH 只映射一个表,基类子类都存放一起,所以,只能在基类的配置中调用 ToTable 方法设置表名,在配置派生类时不要再映射表名了;

B、主键只能在基类的配置中设置,不能在派生类的配置中设置。如上面的例子,如果在配置 DeriveType 类上设置主键,比如这样。

// 基类
public class BaseType
{
    public int Bid { get; set; }
}

// 派生类
public class DeriveType : BaseType
{
    public int Mid { get; set; }
    public double SValue { get; set; }
}

//--------------------------------------------------
// 配置基类实体
modelBuilder.Entity<BaseType>(ent =>
{
    ent.ToTable("tb_zbzb");     // 设置表名
});
// 配置派生类实体
modelBuilder.Entity<DeriveType>(d =>
{
    d.HasKey(x => x.Mid);      // 主键
});

运行后,就会报以下错误:

A key cannot be configured on 'DeriveType' because it is a derived type. 
The key must be configured on the root type 'BaseType'.
If you did not intend for 'BaseType' to be included in the model,
ensure that it is not referenced by a DbSet property on your context instance,
referenced in a configuration call to 'ModelBuilder', or referenced from a navigation on a type that is included in the model.

上面例子,构建了以下数据库模型。

Model: 
  EntityType: BaseType
    Properties: 
      Bid (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)
    Keys: 
      Bid PK
  EntityType: DeriveType Base: BaseType
    Properties: 
      SValue (double) Required

在 DeriveType 实体的模型信息中,Base: BaseType 表示它的基类是 BaseType。说明 EF Core 已经自动配置好继承关系。如果你想手动配置实体的继承关系,可以在配置派生类实体时调用 HasBaseType 方法。

modelBuilder.Entity<DeriveType>()
            .HasBaseType<BaseType>();

调用 HasBaseType 方法所指的基类必须是 .NET 代码中确实存在继承关系的类。比如,如果 DeriveType 不是 BaseType 的派生类,那么,调用 HasBaseType 方法会报错。EF Core 会验证 .NET 类型是否存在继承关系。

我们还会发现,EF Core 为 BaseType 实体添加了一个叫 Discriminator 的影子属性,它会被映射到数据表中的某一列。Discriminator 叫鉴别器,或叫判别器。作用是标识类型的——区分这条数据记录是哪个实体类型的。默认实现是在插入数据时存入实体名称。

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

接下来,咱们用另一个示例来研究一下如何自定义鉴别器。

先定义一个 Shape 基类。

public abstract class Shape
{
    public int ShapeID { get; set; }
}

戴上老花镜看清楚,它,是一个抽象类。你没看错,EF Core 是允许向模型添加抽象类的,但随后你必须添加其实现类。毕竟银河系人都知道,抽象类无法生娃……不,是无法实例化。

然后,有几个类派生自 Shape 类。

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
}

public class Triangle : Shape
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
}

public class Circle : Shape
{
    public float R { get; set; }
}

下面代码定义数据库上下文,实现模型配置。

public class MyContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 配置数据库连接
        SqlConnectionStringBuilder cb = new();
        cb.DataSource = "(localdb)\\mssqllocaldb";
        cb.InitialCatalog = "testdb";
        cb.IntegratedSecurity = true;
        optionsBuilder.UseSqlServer(cb.ConnectionString);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Shape>(ent =>
        {
            // 配置表名
            ent.ToTable("tb_shapes");
            // 继承映射策略(可省略)
            ent.UseTphMappingStrategy();
            // 主键
            ent.HasKey(s => s.ShapeID).HasName("PK_Shapes");
            // 自定义鉴定器
            ent.HasDiscriminator<int>("TypeId")
               .HasValue<Shape>(1)
               .HasValue<Rectangle>(2)
               .HasValue<Triangle>(3)
               .HasValue<Circle>(4);
        });
    }
}

其他代码你已经很熟了,不用看了,咱们重点关心这里:

ent.HasDiscriminator<int>("TypeId")
   .HasValue<Shape>(1)
   .HasValue<Rectangle>(2)
   .HasValue<Triangle>(3)
   .HasValue<Circle>(4);

HasDiscriminator 方法用来自定义类型鉴定器,在本示例中,它是 int 类型。名字叫 TypeId,EF Core 会自动帮我们往 Shape 实体添加影子属性(TypeId)。这么一搞,咱们就覆盖了默认的类型标识方法,HasValue 方法用于为各个派生类实体设置鉴别值。

在本示例中,Shape 类的对象我们用1表示,Rectangle 对象我们用 2 表示,Circle 对象用 4 表示。实际上 Shape 类型的标识可能不会出现,我们无法把抽象类的实例添加到数据集合中。

你也看到了,上述代码在配置完 Shape 类后,并没有去添加 Rectangle 等几个派生类。这是因为在自定义类型鉴别器时,HasValue 方法提到了几个派生类,于是,EF Core 就帮我们添加了。

当然,我们也可以像常规实体一样,配置自定义的属性与列之间的映射。只要注意 ToTable 方法只能在基类 Shape 中配置。简单地说就是:表名、主键都必须在基类中配置。

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Shape>(ent =>
        {
            // 配置表名
            ent.ToTable("tb_shapes");
            // 继承映射策略(可省略)
            ent.UseTphMappingStrategy();
            // 属性映射
            ent.Property(s => s.ShapeID).HasColumnName("sid");
            // 主键
            ent.HasKey(s => s.ShapeID).HasName("PK_Shapes");
            // 自定义鉴定器
            ent.HasDiscriminator<int>("TypeId")
               .HasValue<Shape>(1)
               .HasValue<Rectangle>(2)
               .HasValue<Triangle>(3)
               .HasValue<Circle>(4);
            ent.Property("TypeId").HasColumnName("_type_id");
        });

        modelBuilder.Entity<Rectangle>(ent =>
        {
            ent.Property(s=>s.Width)
                .HasColumnName("rect_wid")
                .HasColumnType("decimal")
                .HasPrecision(8, 2);        // 精度
            ent.Property(m=>m.Height)
                .HasColumnName("rect_hei")
                .HasColumnType("decimal")
                .HasPrecision(8, 2);
        });

        modelBuilder.Entity<Circle>(ce =>
        {
            ce.Property(b=>b.R).HasColumnName("cir_r")
                .HasColumnType("decimal")
                .HasPrecision(10, 3);
        });

        modelBuilder.Entity<Triangle>(ent =>
        {
           ent.Property(a=>a.A).HasColumnName("a_len"); 
           ent.Property(a=>a.B).HasColumnName("b_len"); 
           ent.Property(a=>a.C).HasColumnName("c_len"); 
        });
    }

然后,创建的数据表如下:

CREATE TABLE [tb_shapes] (
    [sid] int NOT NULL IDENTITY,
    [_type_id] int NOT NULL,
    [cir_r] decimal(10,3) NULL,
    [rect_wid] decimal(8,2) NULL,
    [rect_hei] decimal(8,2) NULL,
    [a_len] int NULL,
    [b_len] int NULL,
    [c_len] int NULL,
    CONSTRAINT [PK_Shapes] PRIMARY KEY ([sid])
);

咱们试着每种类型的实体都添加一条记录。

using(var context = new MyContext())
{
    // 获取数据集合
    DbSet<Rectangle> rects = context.Set<Rectangle>();
    DbSet<Triangle> triangles = context.Set<Triangle>();
    DbSet<Circle> circles = context.Set<Circle>();
    // 添加数据
    rects.Add(new Rectangle
    {
        Width = 120.0d,
        Height = 90.0d
    });
    circles.Add(new Circle
    {
        R = 19.2f
    });
    triangles.Add(new Triangle
    {
        A = 16,
        B = 14,
        C = 22
    });
    // 提交到数据库
    context.SaveChanges();
}

执行之后,数据库中保存的数据如下图所示。

image

 由于 EF Core 会将整个继承链作为一个组来管理,所以在获取数据集的时候,可以使用 Shape 类,就像这样。

using(MyContext context = new())
{
    // 获取兼容性数据集合
    DbSet<Shape> shapes = context.Set<Shape>();
    // 添加数据
    shapes.AddRange(
        new Rectangle
        {
            Width = 120.0d,
            Height = 90.0d
        },
        new Circle
        {
            R = 19.2f
        },
        new Triangle
        {
            A = 16,
            B = 14,
            C = 22
        });
    // 提交数据库
    context.SaveChanges();
}

你可以向 shapes 集合添加 Rectangle、Circle 或 Triangle 类。这是利用了派生类实例可以给基类赋值的特点。

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

TPH 有两种特殊的列映射。先说第一种:来自不同派生类的属性映射到同一列。大前提是属性的数据类型要一样,这个好理解,如果两个类中的属性,一个是 string,另一个是 double,这映射到同一列,那数据库中这个列要用什么类型?显然是不行的(除非你写了值转器,并且保证数据库中存入这个列的内容可兼容)。

下面是个常规例子,一个抽象基类,两个同级派生类。

public abstract class DevBoard
{
    public int BoardID { get; set; }
}

public class PicoBaord : DevBoard
{
    public int UartNum { get; set; }
}

public class BeeBoard : DevBoard
{
    public int I2sNum { get; set; }
}

这里咱们要做一个特殊处理,就是把 PicoBoard的 UartNum 和 BeeBoard 类的 I2sNum 属性都映射到 bus_num 列。两个属性都是 int 类型,可以共享其数据。

下面是模型配置。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<DevBoard>(bde =>
    {
        bde.ToTable("tb_boards");
        bde.HasKey(b => b.BoardID).HasName("PK_Boards");
        // 配置鉴别器
        bde.HasDiscriminator<string>("EntityType")
         .HasValue<PicoBaord>("PICO")
         .HasValue<BeeBoard>("BEE");
        // 配置属性
        bde.Property(a => a.BoardID).HasColumnName("bid");
        bde.Property("EntityType").HasColumnName("__type")
            .HasMaxLength(5);
    });
    modelBuilder.Entity<PicoBaord>(pb =>
    {
       pb.Property(s=>s.UartNum)
            .HasColumnName("bus_num"); 
    });
    modelBuilder.Entity<BeeBoard>(bet =>
    {
       bet.Property(a=>a.I2sNum)
            .HasColumnName("bus_num"); 
    });
}

在这个例子中,老周使用字符串“PICO”、“BEE”来区分两个类实体。

接下来,咱们为这两个类分别存入两条数据。

using MyContext context = new();
context.Database.EnsureCreated();
// 每个类各添加两条记录
DbSet<DevBoard> boards = context.Set<DevBoard>();
boards.AddRange(
    new PicoBaord{ UartNum = 2 },
    new PicoBaord{ UartNum = 1 });
boards.AddRange(
    new BeeBoard{ I2sNum = 1 },
    new BeeBoard{ I2sNum = 3 }
);
// 提交数据库
context.SaveChanges();

最后,数据库中的内容如下图所示。

image

第二种情况就是:派生类之间有名称相同的属性,且映射的列名不同。这个不要求数据类型相同,因为它们没任何联系,仅仅名字相同罢了,属于不共享状态。

咱们把上面例子的实体类改一下,让每个派生类都有 UartNum 和 I2cNum 属性。

public abstract class DevBoard
{
    public int BoardID { get; set; }
}

public class PicoBaord : DevBoard
{
    public int UartNum { get; set; }
    public int I2cNum{get;set;}
}

public class BeeBoard : DevBoard
{
    public int UartNum { get; set; }
    public int I2cNum{get;set;}
}

但配置时注意,它们将映射到不同的列。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 这部分不变
    ……

    modelBuilder.Entity<PicoBaord>(pb =>
    {
        pb.Property(s => s.UartNum)
            .HasColumnName("pico_uart_num");
        pb.Property(k => k.I2cNum)
            .HasColumnName("pico_i2c_num");
    });
    modelBuilder.Entity<BeeBoard>(bet =>
    {
        bet.Property(a => a.UartNum)
            .HasColumnName("bee_uart_num");
        bet.Property(f => f.I2cNum)
            .HasColumnName("bee_i2c_num");
    });
}

同样地,咱们也分别添加两条数据。

DbSet<DevBoard> boards = context.Set<DevBoard>();
boards.AddRange(
    new PicoBaord{ UartNum = 2, I2cNum = 2 },
    new PicoBaord{ UartNum = 1, I2cNum = 3 });
boards.AddRange(
    new BeeBoard{ UartNum = 4, I2cNum = 2 },
    new BeeBoard{ UartNum = 3, I2cNum = 3 }
);
// 提交数据库
context.SaveChanges();

数据库中的数据如下图所示。

image

好了,今天的水文就水到这里了。