单表继承(TABLE_INHERIT)
上一页讲的是默认形态:父表 + 每个子类一张独立子表(多表继承)。ts-grm 还提供另一种存储策略——单表继承(STI):所有子类的列合并到同一张父表上,行与行的区别只靠判别列。
为什么有两种策略?两种都不是"更高级",而是适用场景不同。先明白 STI 解决什么,再看怎么用。
场景直觉:什么时候该用 STI
问自己两个问题:
- 子类之间差异多不多? 差异小(每个子类只多一两个字段)→ STI 合适;差异大(字段成堆)→ 独立子表干净;
- 查询通常从哪个入口? 大量查询从基类出发、且要带出分支字段 → STI 一条 SQL 搞定(左 join 子表的成本全省);总是查某一个子类 → 独立子表更紧凑。
典型:用户账号——企业会员 / 个人会员 共享 id/name,企业多一个税号、个人多一个昵称,且后台经常"按所有会员"查——这就是单表继承的教科书场景(沙盒 member 表即为此建)。
声明:一样的三件套,多一个 TABLE_INHERIT
// 讲解片段(完整可运行版见下节与工作区)
import { model, prop, TABLE_INHERIT, DISCRIMINATOR_VALUE_MODEL_NAME } from '@ts-grm/core'
// ① 抽象基类:公共列 + 判别列 TYPE —— 和默认形态完全一样
const Member = model.abstract("Member", "id", class {
id = prop.i64()
name = prop.str(50)
}, ctx => {
ctx.table({ discriminator: "TYPE" })
})
// ② 子类:唯一区别是 name: TABLE_INHERIT —— 表示"与父类共用一张表"
const CompanyMember = model.extends(Member)(
"CompanyMember",
class {
taxNo = prop.str(50)
},
ctx => ctx.table({
name: TABLE_INHERIT, // ★ 单表继承的关键
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME,
})
)
const PersonalMember = model.extends(Member)(
"PersonalMember",
class {
nickname = prop.str(50)
},
ctx => ctx.table({
name: TABLE_INHERIT,
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME,
})
)
对比默认形态(上一页),声明只差一行:子类 ctx.table({ name: TABLE_INHERIT }) 就合表,不写就是独立子表。
物理形态:一张表,子类列混排
示例中的 member 表(单表、无虚拟):
| id | name | TYPE | TAX_NO | NICKNAME |
|---|---|---|---|---|
| 1 | Acme Inc | CompanyMember | CN-1001 | — |
| 2 | Ada Lovelace | PersonalMember | — | ada_l |
| 3 | Beta Ltd | CompanyMember | CN-2002 | — |
| 4 | Alan Turing | PersonalMember | — | alan_t |
- 公共列(id/name)+ 判别列(TYPE)+ 两个子类列(TAX_NO/NICKNAME)全部在一张表;
- 子类列对不适用行是
NULL(企业行没有昵称、个人行没有税号); - 建表带有多态 check 约束(
createSchema会生成等价物):check(type in ('CompanyMember','PersonalMember'))——数据库层面就挡住非法类型行; - 右侧「数据库」抽屉里
member是一个 tab;由于它有判别列,抽屉在表头提供「模型视角」过滤(全部 / CompanyMember / PersonalMember)——这是对真实单表的过滤视图(相当于WHERE TYPE=...),不是伪表。
查询:单表直取,没有 join
基类查询 + 分支取形,SQL 是一家子共桌:
// ① 导入:model/prop/dto 与判别常量;sqlClient 是示例提供的客户端
import { model, prop, dto, TABLE_INHERIT, DISCRIMINATOR_VALUE_MODEL_NAME } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'
// ② 抽象基类 + 两个子类(同上声明,完整可运行版)
const Member = model.abstract("Member", "id", class {
id = prop.i64()
name = prop.str(50)
}, ctx => {
ctx.table({ discriminator: "TYPE" })
})
const CompanyMember = model.extends(Member)(
"CompanyMember",
class { taxNo = prop.str(50) },
ctx => ctx.table({ name: TABLE_INHERIT, discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME })
)
const PersonalMember = model.extends(Member)(
"PersonalMember",
class { nickname = prop.str(50) },
ctx => ctx.table({ name: TABLE_INHERIT, discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME })
)
// ③ 基类查询 + 分支取形(和 MTI 一样的 $instanceOf 写法)
const MemberView = dto.view(Member, c => [
c.id,
c.name,
c.$instanceOf(CompanyMember, c => [c.taxNo]),
c.$instanceOf(PersonalMember, c => [c.nickname]),
])
const members = await sqlClient.createQuery(Member, (q, m) => {
return q.select(m.fetch(MemberView))
}).fetchList()
console.log(members)
// 结果(沙盒种子数据):
// [
// { id: 1, name: 'Acme Inc', __typename: 'CompanyMember', taxNo: 'CN-1001' },
// { id: 2, name: 'Ada Lovelace', __typename: 'PersonalMember', nickname: 'ada_l' },
// { id: 3, name: 'Beta Ltd', __typename: 'CompanyMember', taxNo: 'CN-2002' },
// { id: 4, name: 'Alan Turing', __typename: 'PersonalMember', nickname: 'alan_t' }
// ]
运行后看结果面板的 SQL 追踪:select ... TAX_NO, NICKNAME from MEMBER tb_1_——只有一张表、零 join。子类直查(createQuery(CompanyMember))则是 from MEMBER where TYPE='CompanyMember'。
与默认形态(MTI)的完整对比
| 维度 | 独立子表(默认/MTI) | 单表继承(TABLE_INHERIT/STI) |
|---|---|---|
| 物理表 | 父表 + 每子类一张 | 一张表 |
| 子类列 | 独立子表上,非空 | 父表上,对不适用行 NULL |
| 基类查询带分支 | left join 各子表 | 同表直取,零 join |
| 子类直查 | 子表紧凑(该类型行全在) | WHERE TYPE=... 过滤 |
| 建表自动约束 | 父表多态 check + 子表 FK 级联 + 子表判别 check(多层时) | 一张表的多态 check |
| 适用 | 子类差异大、常按子类查 | 差异小、常按基类查/统计全部 |
| 代价 | 多张表、join 成本(小数据量无所谓) | 子类列 NULL 冗余、单表变宽 |
一句话:差异小、按基类查 → STI;差异大、按子类查 → 默认的独立子表。中间地带看团队习惯,两种都正宗,混用也行(同一套模型里部分子类合表、部分独立,由各自 ctx.table 决定)。
沙盒说明
member 表是示例中的真实单表(含多态 check 约束与 4 行数据);book_store 链保留默认形态(三张独立表)——你可以同时在抽屉里对照两种形态的差别。
下一步
两种继承存储都讲完了。下一节看复合值 embedded:另一种"结构复用",但与继承完全不同(不改变表、无判别)。