# 继承与多态

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

- **多态**（同一概念多种形态、按类型取不同字段）→ 用**继承**；
- **只是复用字段**（没有"一类多型"的需求）→ 用**复合值 embedded**，不要为了省重复而继承。

先声明：**ts-grm 继承的默认形态是"父表 + 每个子类一张独立子表"**（类似每类一表）；也可以显式 `TABLE_INHERIT` 合并成一张表（单表继承），见下节对照。

## 声明：抽象基类 + 子类

```ts
// 讲解片段（完整可运行版见下节与工作区）
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 子表**取回的：

```ts workspace
// ① 导入：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)
```

运行结果（沙盒种子数据）：

```json
[
  { "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`：

```ts
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）**——子类列合并在同一张表。下一节单独讲它：什么场景该用、声明差在哪、单表查询形态。
