TSts-grm

回归总结:同一输入 × 不同选项

本章按「一页一种设定」拆开了 save 的三档选项。这一页把它们摆回同一张桌子:同一个输入,只动选项, 看会发生什么。对比不是为了背表,而是为了建立直觉——save 是「输入决定形状、选项决定落法」

⚠️ 设计预览:以下行为矩阵为类型设计 + 命名语义的推断,待上游发布后逐格核销。

行为矩阵(设计形态)

以「书 + 作者集合」为例:

// 同一输入:{ id: 10n, name: "新书", authors: [{ id: 5n }, { id: 7n }] }
档位选项取值这次保存的结果
rootINSERT无条件新增一行 10
rootINSERT_IF_ABSENTid=10 不存在才插入(已存在→跳过)
rootUPDATE按 id=10 更新(不存在→不插入)
rootUPSERT10 在→更新;不在→插入(幂等,可重放)
rootNON_IDEMPOTENT_UPSERT每次执行都实写
associated.authorsAPPEND_IF_ABSENT只在中间表补 {5,7} 的「缺失项」
associated.authorsREPLACE集合对齐为 {5,7}
dissocation.books(o2m)SET_NULL输入外的子行外键置空
dissocation.books(o2m)DELETE输入外的子行物理删除
view 附带BookView返回 data 写后回读

before → act → after 的完整闭环

写入语义的正确验证姿势(本章贯穿的叙事模板):

// ① before:查询现状(或直接看 DB 抽屉)
// const before = await sqlClient.createQuery(Book, (q, b) =>
//     q.select(b.fetch(BookView))).fetchList()

// ② act:一次 save(root + associated + view 全开)
// const result = await sqlClient.save(BookInput, payload, {
//     root: "UPSERT",
//     associated: { "authors": "APPEND_IF_ABSENT" },
//     view: BookView,
// })

// ③ after 三方互证
//  - result.affected:$total + 每路径计数,证明「写到了哪几层」
//  - result.data:写后回读形状
//  - DB 抽屉:切到对应表看真实行
//  再加一条:SQL 跟踪里的多条语句——一次 save 背后的 INSERT/UPDATE 一览无余

选型速记

  • 想「有则更无则增」→ root: "UPSERT" + 合理 key();要「每次实写」→ NON_IDEMPOTENT_UPSERT
  • 关联只增不减 → APPEND_IF_ABSENT;彻底对齐 → VIOLENTLY_REPLACE/REPLACE
  • o2m 的孤儿行 → 显式 dissocation,别用默认(默认行为发布后核销,先显式声明更稳);
  • 写完要展示 → 带 viewdata 直接可用。

自由发挥

把上面的代码块「+ 添加到工作区」,进入沙盒自由模式:随手上一个输入,把三档选项各试一遍, 配 SQL 跟踪看每条语句——选项的差异最终都会变成 SQL 的差异。等上游发布 save 运行时后, 本页示例即可从设计预览解封为可直接运行。