关联保存模式(associated)
当输入 DTO 内嵌了关联(.with()、以及 $ref() 携带的关联 id),保存时的第二个问题就是:这些关联怎么落。
associated 选项按路径为每个关联单独指定模式,六个字符串字面量:
// ① AssociatedSaveMode:当前 API 设计
import type { AssociatedSaveMode } from '@ts-grm/core'
// type AssociatedSaveMode =
// | "REPLACE" // 替换为输入内容
// | "MERGE" // 与现有成员合并
// | "APPEND" // 追加
// | "APPEND_IF_ABSENT" // 按 key 判断,不存在才追加
// | "UPDATE" // 按 key 定位并更新
// | "VIOLENTLY_REPLACE" // 暴力整体替换
⚠️ 设计预览:枚举与调用形态以当前 API 设计为准;各值的行为边界待正式实现后实测确认。 REPLACE 与 VIOLENTLY_REPLACE 的差异(是否按 key 保守合并、是否物理删除未被输入覆盖的行)是发布后重点核销项。
路径键语法:"关联名" / "关联名.子关联名"
选项的 key 是相对根对象的关联路径,. 连接嵌套层级:
// ② 带嵌套集合的 Input(上游测试写法示意:树节点 + 子节点 + 子节点的 tags)
// const INPUT = dto.input(TREE_NODE, c => [
// c.name.key(),
// c.childNodes.with(c => [
// c.name.key(),
// c.$instanceOf(ITEM, c => [c.tags]),
// c.childNodes.with(c => [c.name]),
// ]),
// ])
// save(INPUT, payload, {
// associated: {
// "childNodes": "VIOLENTLY_REPLACE", // 一级集合:整体替换
// "childNodes.tags": "APPEND_IF_ABSENT", // 嵌套路径:子集合按 key 追加
// },
// })
路径写错(拼错关联名、或指向非内嵌关联)会触发 InputAssociationMembers 的类型约束,直接编译报错——选项键集合 = 输入里内嵌的关联集合。
沙盒模型上的 m2m 例子
沿用全书模型:Book.authors 是 m2m(中间表 book_author_mapping)。给「书 × 作者」的写入配上关联模式:
// ③ 输入:书 + 作者集合(内嵌)
// import { model, prop, dto } from '@ts-grm/core'
// 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"],
// })
// })
// const Author = model("Author", "id", class {
// id = prop.i64()
// name = prop.embedded({ firstName: prop.str(50), lastName: prop.str(50) })
// })
//
// const BookInput = dto.input(Book, c => [
// c.id.key(),
// c.name,
// c.authors.$ref("authors", c => [c.id]),
// ])
// ④ 同样的输入,不同的关联模式(设计形态)
// const payload: TypeOf<BookInput> = { id: 1n, name: "…", authors: [{ id: 5n }, { id: 7n }] }
// await sqlClient.save(BookInput, payload, {
// root: "UPSERT",
// associated: { "authors": "APPEND_IF_ABSENT" }, // 只添加中间表里还没有的({5,7})
// })
// await sqlClient.save(BookInput, payload, {
// root: "UPDATE",
// associated: { "authors": "REPLACE" }, // 集合替换为 {5,7}
// })
| 模式 | 直觉行为 | 适用 |
|---|---|---|
APPEND | 追加 | 作者列表增长 |
APPEND_IF_ABSENT | key 不存在才追加(幂等) | 可重放的「加关联」 |
UPDATE | 按 key 更新集合成员本体 | 成员字段要改 |
MERGE | 与现有集合合并 | 并列场景 |
REPLACE | 替换为输入内容 | 「以输入为准」 |
VIOLENTLY_REPLACE | 暴力整体替换 | 「彻底对齐」 |
组合使用:不同路径不同策略
// ⑤ 一页一设定:同一输入、按路径差异化(设计形态)
// await sqlClient.save(INPUT, payload, {
// root: "UPSERT",
// associated: {
// "childNodes": "VIOLENTLY_REPLACE", // 顶层集合彻底对齐
// "childNodes.tags": "APPEND_IF_ABSENT", // 子集合只增不减
// },
// })
这也是「一张保存背后多条 SQL」的主要来源:根一行、每层关联各自成语句,affected 结果里会给出每条路径的计数(见「保存并回读」)。