12.9 具体表继承

为每个具体类建立包含全部继承字段的表,用字段重复换取单类读取简单。

学习目标

  • 能解释为什么 EngineerManager 各自成表时必须重复公共字段,并指出这种重复换来了什么
  • 能修改一段 TypeScript 映射代码,让具体类型安全地写入各自表,并保留统一标识
  • 能回答:当系统需要跨类型列表或频繁修改公共字段时,为什么应重新评估具体表继承

为什么 12.9 具体表继承 值得单独学习

先想象一张员工档案:工程师和经理都需要姓名、入职日期和编号,但它们的专属信息不同。如果每次只查一种员工,档案可以放在一张完整的清单里,读取路径短;代价是两种清单都要重复维护那些共同信息。

这一章解决的是“共同信息放在哪里、不同类型怎样各自保存”的问题。没有清楚的落表规则,新增一个公共字段时可能只改了一张清单,读取两种员工便会得到不同版本;需要把所有员工放在一起时,也可能不得不拼接多张清单。

本页依据 Martin Fowler 的作者图书页与模式目录独立重写,不复现原书正文、插图或代码。目录只限定本页讨论的模式边界,下面的员工案例、实验、代码与练习均为课程原创。

先建立直觉:一棵继承树怎样落成两张表

固定一个很小的例子:Employee 只规定 idnamehiredAtEngineer 追加 skillManager 追加 budget. 如果对象必须独立读取,数据库可以分别拥有 engineersmanagers 两张完整表,而不再建立一张保存 Employee 公共部分的父表。

这种安排把一次读取变成“直接打开目标表”:查工程师不需要连接父表,也不会先取出一行再判断哪一列属于哪种类型。它也把维护责任复制了两份:公共字段的定义、索引和迁移必须在两张表上保持一致。图示会把这两个收益与代价放在同一条证据链上。

具体表继承的核心模型

每张具体表都携带公共字段

的关键是“具体类型独立成行”。engineers 保存 idnamehired_atskillmanagers 保存同样的前三列,再加上 budget。没有 employees 父表,也不靠一条外键把两张表拼成一个对象。

重复不是疏忽,而是这条取舍的价格。单类型详情页可以直接命中一张表;公共字段却有多个物理来源,表约束、索引和迁移脚本必须把它们当成同一份契约来维护。

读取简单,公共字段变化变贵

把公共列复制到多个具体表会形成。它不一定意味着坏数据:在边界清晰、类型数量稳定时,重复列可以换来直白的查询与更少的连接。

真正危险的是演化。若 hired_at 从日期改成带时区的时间戳,迁移必须覆盖两张表;若只改了 engineers,同一个 Employee 概念就出现两套存储语义。公共字段越多、子类型越多,迁移的检查清单越长。

跨类型读取必须显式拼接

把工程师和经理放到同一个列表,需要一次。典型做法是让两张表各自投影出相同的公共列,再用追加结果,并加上 kind 区分来源。

这一步不是免费的抽象:每增加一种具体类型,就要扩展查询、分页排序和权限过滤。若跨类型列表是最主要的用例,直接把每种类型拆表的收益可能抵不过这个组合成本。

设计实验:先看证据,再选映射

目录单元到教学证据

本章只覆盖目录单元 12.9 具体表继承,单元键为 poeaa24-pattern-20-concrete-table-inheritance,模式族为 mapping。证据不是“某次 INSERT 成功”,而是同时检查公共字段的一致性、具体类型的直接读取、跨类型组合查询和下一次迁移的覆盖范围。

订单后台的员工列表

把员工后台拆成三个验收问题:详情页是否只访问一个具体表;新增员工时公共字段是否写入正确表;跨类型列表是否能标出来源并稳定排序。下面的步进图保留同一组 Employee → Engineer / Manager 数据,只改变当前要验收的边界。

先预测:如果在 engineers 表新增 hired_at 的时区约束,但忘记修改 managers,哪一个证据最先暴露不一致?动手切换每一步,观察“少一次 JOIN”如何同时带来“多一份迁移”的责任。

分步1 / 3

步骤 1:具体类型各自拥有完整表

先把 Employee 的公共字段和两个专属字段写在同一张草图上。engineersmanagers 各自包含 idnamehired_at,所以单类详情可以直接读一张表;图中没有父表,也没有等待拼接的外键。

Concrete Table Inheritance:具体类各自成表每个具体类型自带公共字段,不建立 Employee 父表对象模型Employeeid · name · hiredAtEngineer+ skillManager+ budgetengineersid | name | hired_atskill: "backend"公共字段 + 专属字段managersid | name | hired_atbudget: 50000公共字段 + 专属字段具体类型路由直接读取SELECT *FROM engineers一张表 · 少一次 JOIN适合单类详情公共列已有两份迁移需同步维护步骤 1 · 单类型读取Engineer 和 Manager 都能直接命中一张完整表,代价是公共列出现两份。重复字段换取单类读取直接;跨类型查询与公共字段迁移必须显式承担成本
具体表继承不建立父表:每张具体表都包含公共字段;单类读取更直白,但 UNION ALL 与成对迁移必须有明确边界。

代码对照:让每个具体表的映射可检查

下面的 TypeScript 只表达映射边界,不绑定某个 ORM。toRow 保证每个具体对象带上 kind 和统一 id;持久化层再根据 kind 选择表。公共字段集中在 base,让代码审查时能看见两种写入是否遗漏同一列。

type Base = { id: string; name: string; hiredAt: string };
type Employee =
  | (Base & { kind: "engineer"; skill: string })
  | (Base & { kind: "manager"; budget: number });
 
function toRow(employee: Employee) {
  const base = {
    id: employee.id,
    name: employee.name,
    hired_at: employee.hiredAt,
  };
  return employee.kind === "engineer"
    ? { table: "engineers", row: { ...base, skill: employee.skill } }
    : { table: "managers", row: { ...base, budget: employee.budget } };
}

这段代码没有试图把两张表伪装成一张表:它明确写出表名和类型分支。生产实现还应在数据库层为两张表声明相同的 idnamehired_at 约束,并为跨类型列表提供单独的查询函数,而不是让每个调用方手写 UNION。

常见误区

选择与拒绝矩阵

评审问题选择具体表继承的证据应拒绝或改用其他方案的信号
单类读取详情请求大多只针对一种具体类型,且不需要父表 JOIN最主要的页面总是混合展示所有子类型
字段变化子类型数量稳定,公共字段迁移可以成对审查公共字段频繁改动,迁移常漏掉某张表
查询跨类型查询少,可以集中维护 UNION ALL跨类型筛选、排序和分页已经是核心路径
身份各表使用同一标识生成与唯一性策略不同表的 ID 规则不同,合并结果无法稳定去重
替代已比较单表继承、类表继承或专门读模型只是因为 ORM 默认配置而跳过成本评估

选择的标准不是“有没有继承关系”,而是单类型读取的收益是否大于重复迁移和跨类型查询的成本。评审卡至少记录具体表数量、公共字段数量、跨类型列表比例和一次迁移需要触及的表数;这些数字变化时,重新做一次选择。

本章小结

  • 具体表继承让每种具体类型拥有一张包含公共字段的完整表。
  • 单类型读取少一次 JOIN,但公共字段、约束和迁移要重复维护。
  • 跨类型列表需要统一投影并显式 UNION ALL,不能依赖隐式父表。
  • 统一标识和公共字段契约是多张具体表仍可协作的前提。
  • 公共字段变化频繁或跨类型查询变成主路径时,应重新评估映射策略。

本章练习

练习

问题 1: EmployeeidnamehiredAtEngineerskillManagerbudget。若使用具体表继承,请列出 engineersmanagers 各自必须拥有的字段,并说明为什么不能只把公共字段放在 employees 表。

问题 2: 修改下面的函数:当 kindmanager 时写入 managers,同时保留公共字段;当类型不是这两种时直接失败。

function persist(employee: Employee) {
  const base = { id: employee.id, name: employee.name };
  if (employee.kind === "engineer") {
    return db.insert("engineers", { ...base, skill: employee.skill });
  }
  return db.insert("employees", base);
}

问题 3: 后台要求按 hired_at 展示所有员工。你会怎样组织跨类型查询?当公共字段迁移频繁时,哪一个信号会让你拒绝继续使用具体表继承?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

具体表继承

每一种可以创建的具体类型单独拥有一张表,并把父类型字段复制进去。

字段冗余

同一个业务字段在多张表中各保存一份,需要一起修改和校验的结构成本。

多态查询

面向共同父类型,把多个具体类型的记录整理成一个结果集的查询。

UNION ALL

把多条查询结果直接追加在一起且不主动去重的 SQL 操作;使用前要保证列的含义和顺序一致。

前后导航

讨论

评论区加载中…