TSts-grm

关联

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

关联外键位置本模型视角
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。

下一步

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