回归总结:同一输入 × 不同选项
本章按「一页一种设定」拆开了 save 的三档选项。这一页把它们摆回同一张桌子:同一个输入,只动选项, 看会发生什么。对比不是为了背表,而是为了建立直觉——save 是「输入决定形状、选项决定落法」。
⚠️ 设计预览:以下行为矩阵为类型设计 + 命名语义的推断,待上游发布后逐格核销。
行为矩阵(设计形态)
以「书 + 作者集合」为例:
// 同一输入:{ id: 10n, name: "新书", authors: [{ id: 5n }, { id: 7n }] }
| 档位 | 选项取值 | 这次保存的结果 |
|---|---|---|
root | INSERT | 无条件新增一行 10 |
root | INSERT_IF_ABSENT | id=10 不存在才插入(已存在→跳过) |
root | UPDATE | 按 id=10 更新(不存在→不插入) |
root | UPSERT | 10 在→更新;不在→插入(幂等,可重放) |
root | NON_IDEMPOTENT_UPSERT | 每次执行都实写 |
associated.authors | APPEND_IF_ABSENT | 只在中间表补 {5,7} 的「缺失项」 |
associated.authors | REPLACE | 集合对齐为 {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,别用默认(默认行为发布后核销,先显式声明更稳); - 写完要展示 → 带
view,data直接可用。
自由发挥
把上面的代码块「+ 添加到工作区」,进入沙盒自由模式:随手上一个输入,把三档选项各试一遍, 配 SQL 跟踪看每条语句——选项的差异最终都会变成 SQL 的差异。等上游发布 save 运行时后, 本页示例即可从设计预览解封为可直接运行。