# 单表继承（TABLE_INHERIT）

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

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

## 场景直觉：什么时候该用 STI

问自己两个问题：

1. **子类之间差异多不多？** 差异小（每个子类只多一两个字段）→ STI 合适；差异大（字段成堆）→ 独立子表干净；
2. **查询通常从哪个入口？** 大量查询从**基类**出发、且要带出分支字段 → STI 一条 SQL 搞定（左 join 子表的成本全省）；总是查某一个子类 → 独立子表更紧凑。

典型：**用户账号**——`企业会员 / 个人会员` 共享 `id/name`，企业多一个税号、个人多一个昵称，且后台经常"按所有会员"查——这就是单表继承的教科书场景（沙盒 `member` 表即为此建）。

## 声明：一样的三件套，多一个 TABLE_INHERIT

```ts
// 讲解片段（完整可运行版见下节与工作区）
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 是一家子共桌：

```ts workspace
// ① 导入：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**：另一种"结构复用"，但与继承完全不同（不改变表、无判别）。
