TSts-grm

继承与多态

多个实体共享字段有两种选择,先分清目的

  • 多态(同一概念多种形态、按类型取不同字段)→ 用继承
  • 只是复用字段(没有"一类多型"的需求)→ 用复合值 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_store4 行(公共列 + TYPE:2 物理 / 2 线上)
physical_book_store2 行(id 1/3:city/street)
online_book_store2 行(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)——子类列合并在同一张表。下一节单独讲它:什么场景该用、声明差在哪、单表查询形态。