关联
关联把表与表连起来。四种方向,一句话记住谁拥有外键:
| 关联 | 外键位置 | 本模型视角 |
|---|---|---|
m2o(多对一) | 本表(拥有方) | 带一个关联对象(store)+ 一个引用键(storeId) |
o2m(一对多) | 对方表 | 带一个集合(books) |
o2o(一对一) | 其中一侧(拥有方) | 带一个关联对象 |
m2m(多对多) | 中间表 | 带一个集合(authors) |
拥有方之外键在取形里同时以「引用键」直取:
c.store(对象)与c.storeId(外键标量) 任选,见「快速上手 → 视图」的引用键小节。
多对一与一对多:同一张外键
// ① 书与书店:多对一在 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 表与数据,此例可运行,完整版本见工作区):
// 书 ↔ 作者: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
中间表还想带自己的字段(如选课成绩、关联时间)?用显式中间实体:
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 为准。
自关联:同一个模型连自己
树、分类、组织架构这类"自己连自己"的模型,传参形态与外关联不同,这是自关联最需要记住的地方:
// 外关联:直接传模型对象
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():
parent = prop.m2o(() => Category) // 外键可空:树根没有父级
外键列名仍是默认的 属性名 + _ID(PARENT_ID),要改名用 .joinColumns(...)。
(o2m / o2o / m2m 的 lambda 形态还会放宽 mappedBy 的键约束:外关联的反向只能指向目标模型上真实存在的关联属性,
自关联则可以指向本模型的任意属性——mappedBy("parent") 因此合法。)
取形与查询:与普通关联没有区别
// 上一级(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 返回:
[{ "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。
下一步
关联之外,模型体系还有两块大内容:继承与多态(同一个概念多种形态)与复合值(一列变成一组列)。先看继承。