# 关联

关联把表与表连起来。四种方向，一句话记住**谁拥有外键**：

| 关联 | 外键位置 | 本模型视角 |
|---|---|---|
| `m2o`（多对一） | **本表**（拥有方） | 带一个关联对象（`store`）+ 一个**引用键**（`storeId`）|
| `o2m`（一对多） | 对方表 | 带一个集合（`books`） |
| `o2o`（一对一） | 其中一侧（拥有方） | 带一个关联对象 |
| `m2m`（多对多） | 中间表 | 带一个集合（`authors`） |

> 拥有方之外键在取形里**同时以「引用键」直取**：`c.store`（对象）与 `c.storeId`（外键标量）
> 任选，见「快速上手 → 视图」的引用键小节。

## 多对一与一对多：同一张外键

```ts
// ① 书与书店：多对一在 Book（外键 STORE_ID），一对多在 BookStore（纯反向）
const BookStore = model("BookStore", "id", class {
    id = prop.i64()
    name = prop.str(100)
    books = prop.o2m(Book).mappedBy("store")
        .orderBy("name", "edition")      // 集合默认排序（取 books 时的默认顺序）
})

const Book = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    edition = prop.i32()
    price = prop.num(10, 2)

    // 拥有方：生成外键列 STORE_ID；.nullable() = 外键可空
    store = prop.m2o(BookStore)
        .joinColumns({ cascade: "DELETE" })   // 删书店级联删书
        .nullable()
})
```

- **`o2m` 永远通过 `mappedBy` 指向对方某个 m2o/o2o 属性名**，本身不产生列；
- `joinColumns({ cascade: "DELETE" })` 声明级联：删书店时删掉它的书（级联方向由拥有方决定）；
- `o2m(...).orderBy(...)` 给集合定**默认排序**，查询取 `books` 时自动带上。

## 多对多：中间表

`m2m` 的常见形态是用**中间表**连接两侧（示例数据已含 `author` / `book_author_mapping` 表与数据，此例可运行，完整版本见工作区）：

```ts
// 书 ↔ 作者：book_author_mapping 中间表（片段：完整可运行版见工作区）
const AUTHOR = model("Author", "id", class {
    id = prop.i64()
    name = prop.str(50)
    books = prop.m2m(BOOK).mappedBy("authors")   // 反向：不必成对声明，方便时再加
})

const BOOK = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    // 拥有侧：声明中间表与两侧的外键列
    authors = prop.m2m(AUTHOR).joinTable({
        name: "book_author_mapping",
        joinThisColumns: ["book_id"],      // 中间表里指向"本侧"的列
        joinTargetColumns: ["author_id"],  // 中间表里指向"对侧"的列
    })
})
```

- 默认命名策略下中间表名、列名可省略（自动推导）；
- 反向 `m2m(...).mappedBy("authors")` 与 o2m 同理，不建任何列；
- m2m 集合的子视图取 **embedded 子路径**要用 `.with` 展开取形（
  `c.authors.with(a => a.name.with(c => [c.lastName]))`）；直接链 `c.author.name.lastName` 会触发
  DTO 构造错误（0.0.12 / 0.0.13 实测一致，上游已澄清：正确用法即以 `.with` 展开，详见上游追踪文档）。

## 多对多带载荷：joinEntity

中间表还想带自己的字段（如选课成绩、关联时间）？用**显式中间实体**：

```ts
const COURSE = model("Course", "id", class {
    id = prop.i64()
    name = prop.str(50)
    // students 经由 LearningLink 实体连接，成绩等字段在链上
    students = prop.m2m(STUDENT).joinEntity({
        model: LEARNING_LINK,           // 中间实体（自带 id/score/student/course）
        joinThisProp: "student",        // 中间实体指向本侧的 m2o 属性
        joinTargetProp: "course",       // 中间实体指向对侧的 m2o 属性
    })
})
```

此时 `LearningLink` 是一个普通实体：既能按关联用，也能直接查它的 `score` 等载荷字段。

## 一对一

`o2o` 语义与 `m2o` 相同，只是外键加了唯一约束（一侧只能有一个）。涉及它时请以当前版本 API 为准。

## 自关联：同一个模型连自己

树、分类、组织架构这类"自己连自己"的模型，**传参形态与外关联不同**，这是自关联最需要记住的地方：

```ts
// 外关联：直接传模型对象
store = prop.m2o(BookStore)

// 自关联：传 lambda，返回本模型（() => Category）
const Category = model("Category", "id", class {
    id = prop.i64()
    name = prop.str(50)
    parent = prop.m2o(() => Category)                       // 拥有方：外键 PARENT_ID → c.parentId
    children = prop.o2m(() => Category).mappedBy("parent")  // 反向集合
})
```

- **自关联必须写 lambda**：`Category` 此刻还在自己的初始化器里，直接写 `prop.m2o(Category)` 编译期就报
  `TS2448: Block-scoped variable 'Category' used before its declaration`；lambda 把求值推迟到模型解析时，那时模型已经建好（即
  "模型 → 定义顺序无关（惰性解析）"讲的懒加载）。
- **反过来，lambda 只留给递归关联**：给外关联写 `prop.m2o(() => BookStore)` 编译能过，但模型解析时会抛错——
  `Lambda argument can only be used for recursive assocaitions, please specify the target model directly`（原文拼写如此）。

`o2o` / `m2m` 的自关联同理（配偶、好友）：`prop.o2o(() => Person).mappedBy("partner")`、
`prop.m2m(() => User).joinTable({ ... })`。

### lambda 形态默认可空

`m2o` 传模型对象默认非空，传 lambda 则默认**可空**——所以自关联的 `parent` 不必再写 `.nullable()`：

```ts
parent = prop.m2o(() => Category)   // 外键可空：树根没有父级
```

外键列名仍是默认的 `属性名 + _ID`（`PARENT_ID`），要改名用 `.joinColumns(...)`。

（`o2m` / `o2o` / `m2m` 的 lambda 形态还会放宽 `mappedBy` 的键约束：外关联的反向只能指向目标模型上真实存在的关联属性，
自关联则可以指向本模型的任意属性——`mappedBy("parent")` 因此合法。）

### 取形与查询：与普通关联没有区别

```ts
// 上一级（m2o）：每行带它的父节点
const UpView = dto.view(Category, c => [c.id, c.name, c.parent.with(c => [c.id, c.name])])
// 下一级（o2m）：每个节点带它的直接子节点
const DownView = dto.view(Category, c => [c.id, c.name, c.children.with(c => [c.id, c.name])])
// 整棵子树：$recursive 自闭合（没有 .with），可 .as("subtree") 改名、.depth(1) 限深
const TreeView = dto.view(Category, c => [c.id, c.name, c.$recursive("children")])
```

以 `(1, root, null) (2, child, 1) (3, grand, 2)` 为例，`TreeView` 返回：

```json
[{ "id": 1, "name": "root", "children": [
    { "id": 2, "name": "child", "children": [
      { "id": 3, "name": "grand", "children": [] } ] } ] },
  { "id": 2, "name": "child", "children": [
    { "id": 3, "name": "grand", "children": [] } ] },
  { "id": 3, "name": "grand", "children": [] }]
```

## 不对称重申

m2o 与 o2m 不需要成对出现（「快速上手 → 视图」讲过）：外键建在哪张表，关联就在哪个模型声明；反向只是便利视角。m2m 同理——只在真正需要"从另一侧出发"时再补 `mappedBy`。

## 下一步

关联之外，模型体系还有两块大内容：**继承与多态**（同一个概念多种形态）与**复合值**（一列变成一组列）。先看继承。
