TSts-grm

关联保存模式(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_ABSENTkey 不存在才追加(幂等)可重放的「加关联」
UPDATE按 key 更新集合成员本体成员字段要改
MERGE与现有集合合并并列场景
REPLACE替换为输入内容「以输入为准」
VIOLENTLY_REPLACE暴力整体替换「彻底对齐」

组合使用:不同路径不同策略

// ⑤ 一页一设定:同一输入、按路径差异化(设计形态)
// await sqlClient.save(INPUT, payload, {
//     root: "UPSERT",
//     associated: {
//         "childNodes": "VIOLENTLY_REPLACE",     // 顶层集合彻底对齐
//         "childNodes.tags": "APPEND_IF_ABSENT", // 子集合只增不减
//     },
// })

这也是「一张保存背后多条 SQL」的主要来源:根一行、每层关联各自成语句,affected 结果里会给出每条路径的计数(见「保存并回读」)。