TSts-grm

单表继承(TABLE_INHERIT)

上一页讲的是默认形态:父表 + 每个子类一张独立子表(多表继承)。ts-grm 还提供另一种存储策略——单表继承(STI):所有子类的列合并到同一张父表上,行与行的区别只靠判别列。

为什么有两种策略?两种都不是"更高级",而是适用场景不同。先明白 STI 解决什么,再看怎么用。

场景直觉:什么时候该用 STI

问自己两个问题:

  1. 子类之间差异多不多? 差异小(每个子类只多一两个字段)→ STI 合适;差异大(字段成堆)→ 独立子表干净;
  2. 查询通常从哪个入口? 大量查询从基类出发、且要带出分支字段 → 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 表(单表、无虚拟):

idnameTYPETAX_NONICKNAME
1Acme IncCompanyMemberCN-1001
2Ada LovelacePersonalMemberada_l
3Beta LtdCompanyMemberCN-2002
4Alan TuringPersonalMemberalan_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:另一种"结构复用",但与继承完全不同(不改变表、无判别)。