继承与多态
多个实体共享字段有两种选择,先分清目的:
- 多态(同一概念多种形态、按类型取不同字段)→ 用继承;
- 只是复用字段(没有"一类多型"的需求)→ 用复合值 embedded,不要为了省重复而继承。
先声明:ts-grm 继承的默认形态是"父表 + 每个子类一张独立子表"(类似每类一表);也可以显式 TABLE_INHERIT 合并成一张表(单表继承),见下节对照。
声明:抽象基类 + 子类
// 讲解片段(完整可运行版见下节与工作区)
import { model, prop, DISCRIMINATOR_VALUE_MODEL_NAME } from '@ts-grm/core'
// ① 抽象基类:公共列 + 判别列 TYPE(父表只有公共列)
const BookStore = model.abstract("BookStore", "id", class {
id = prop.i64()
name = prop.str(100)
}, ctx => {
ctx.table({ discriminator: "TYPE" })
})
// ② 子类:不写 TABLE_INHERIT → 每个子类一张独立表(默认形态)
// idMapping: "STORE_ID" 把继承来的主键属性重定向到子表主键列
const PhysicalBookStore = model.extends(BookStore)(
"PhysicalBookStore",
class {
city = prop.str(50)
street = prop.str(50)
},
ctx => ctx.table({
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME, // 判别值 = 模型名
name: { idMapping: "STORE_ID" },
})
)
const OnlineBookStore = model.extends(BookStore)(
"OnlineBookStore",
class {
url = prop.str(50)
},
ctx => ctx.table({
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME,
name: { idMapping: "STORE_ID" },
})
)
物理形态:三张真实的表
model.extends 不写 TABLE_INHERIT 时,DDL/建表会产出三张独立表(示例数据中的表结构与此一致):
BOOK_STORE —— 父表:ID、NAME、TYPE(判别列)
PHYSICAL_BOOK_STORE —— 子表:STORE_ID(主键→父表 ID)、CITY、STREET
ONLINE_BOOK_STORE —— 子表:STORE_ID(主键→父表 ID)、URL
右侧「数据库」抽屉里对应三个 tab,每张表都是真实存在的物理表、彼此独立:
| tab | 内容 |
|---|---|
book_store | 4 行(公共列 + TYPE:2 物理 / 2 线上) |
physical_book_store | 2 行(id 1/3:city/street) |
online_book_store | 2 行(id 2/4:url) |
- 子表主键同时是外键,
on delete cascade——删除父行连带删除子类行; - 主键列名由
idMapping: "STORE_ID"指定(默认命名策略下的规则可查上游,教学示例用显式映射更清晰); - 判别列
TYPE标识父行属于哪个子类——多态查询据此决定 join 哪张子表。
查询多态:基类入口 + 分支取形
$instanceOf 在视图中按类型取分支字段。下面的示例完整自包含,点「添加到工作区」运行后,看结果面板的 SQL 追踪——分支字段是通过 left join 子表取回的:
// ① 导入:model/prop/dto 与判别常量;sqlClient 是示例提供的客户端
import { model, prop, dto, DISCRIMINATOR_VALUE_MODEL_NAME } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'
// ② 抽象基类:公共列 + 判别列 TYPE
const BookStore = model.abstract("BookStore", "id", class {
id = prop.i64()
name = prop.str(100)
}, ctx => {
ctx.table({ discriminator: "TYPE" })
})
// ③ 子类:独立子表(不写 TABLE_INHERIT);idMapping 指定子表主键列
const PhysicalBookStore = model.extends(BookStore)(
"PhysicalBookStore",
class {
city = prop.str(50)
street = prop.str(50)
},
ctx => ctx.table({
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME,
name: { idMapping: "STORE_ID" },
})
)
const OnlineBookStore = model.extends(BookStore)(
"OnlineBookStore",
class {
url = prop.str(50)
},
ctx => ctx.table({
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME,
name: { idMapping: "STORE_ID" },
})
)
// ④ 基类查询 + 分支取形:物理店多出 city/street,线上店多出 url
const StoreView = dto.view(BookStore, c => [
c.id,
c.name,
c.$instanceOf(PhysicalBookStore, c => [
c.city,
c.street,
]),
c.$instanceOf(OnlineBookStore, c => [
c.url,
]),
])
// ⑤ 执行:4 行;SQL 追踪可见 left join 两张子表分别拉分支列
const stores = await sqlClient.createQuery(BookStore, (q, store) => {
return q.select(store.fetch(StoreView))
}).fetchList()
console.log(stores)
运行结果(沙盒种子数据):
[
{ "id": 1, "name": "Tech Haven", "__typename": "PhysicalBookStore", "city": "Beijing", "street": "Wangfujing Ave" },
{ "id": 2, "name": "Code Corner", "__typename": "OnlineBookStore", "url": "https://code.example" },
{ "id": 3, "name": "Corner Books", "__typename": "PhysicalBookStore", "city": "Shanghai", "street": "Nanjing Rd" },
{ "id": 4, "name": "Cloud Books", "__typename": "OnlineBookStore", "url": "https://cloud.example" }
]
SQL 追踪形如:from BOOK_STORE tb_1_ left join PHYSICAL_BOOK_STORE tb_2_ on tb_1_.TYPE='PhysicalBookStore' and tb_1_.ID = tb_2_.STORE_ID——一条查询跨三张表。
对照:TABLE_INHERIT(单表继承)
想让子类共用父表(所有列堆在同一张表,靠判别列区分行)时,子类显式写 name: TABLE_INHERIT:
const PhysicalBookStore = model.extends(BookStore)(
"PhysicalBookStore",
class { city = prop.str(50); street = prop.str(50) },
ctx => ctx.table({
name: TABLE_INHERIT, // 与父类共用 BOOK_STORE 表
discriminatorValue: DISCRIMINATOR_VALUE_MODEL_NAME,
})
)
两种策略的取舍:
| 维度 | 独立子表(默认) | TABLE_INHERIT 单表 |
|---|---|---|
| 物理表 | 父表 + 每子类一张表 | 一张表 |
| 分支列查询 | left join 子表 | 同表直接取(WHERE TYPE 过滤) |
| 取子类整体 | 子表紧凑(只有该类行) | 全表混存、靠判别列筛 |
| 适用 | 子类差异大、列多 | 差异小、查询点在基类 |
两种形态均被上游测试覆盖:独立子表与单表继承分别有对应测试;
TABLE_INHERIT见 singleTableDerivedRoot 用例(子类直查from BOOK_STORE where TYPE=...) DDL 断言也覆盖了子类列全部落在父表上的形态。
规则要点
model.abstract:抽象基类——声明共享列与判别列;它可以被查询(多态查询恰好从基类入口);- 主键继承 +
idMapping:子类继承父类主键;独立子表默认列名按命名规则生成(模型缩写 +_ID,如PaperBook → PB_ID),示例用name: { idMapping: "STORE_ID" }显式重定向(复合主键用对象形式{ "x": "ID_X", "y.a": "ID_Y_A" }); - 判别三件套必须配对:父类
discriminator↔ 子类discriminatorValue,不配对解析报错;判别列出现的值必须被某子类声明覆盖,否则读取抛Illegal discriminator value; - 直接查询子类(
createQuery(PhysicalBookStore, …))自动带TYPE = '子类名'过滤; - 建表时自动生成约束(
createSchema的产物已由上游测试覆盖):父表生成多态 check(check(TYPE in('…', …)));独立子表生成指向父表(或直接父类,多层继承时)的外键on delete cascade;子类还有后续分支时,子表自身也会带判别列与 check; - 多层继承可继续 extends(如
Book → ElectronicBook → PdfElectronicBook),每层独立子表的 FK 指向直接父类表。
下一步
独立子表(默认)讲完了,还有一种并行的存储策略:单表继承(TABLE_INHERIT)——子类列合并在同一张表。下一节单独讲它:什么场景该用、声明差在哪、单表查询形态。