TSts-grm

根保存模式(root)

sqlClient.save(input, payload, options) 的第一个选项是 root,决定根对象(Input 声明所挂的那个模型)如何落库。五个值全部是字符串字面量联合:

// ① RootSaveMode:当前 API 设计
import type { RootSaveMode } from '@ts-grm/core'

// type RootSaveMode =
//   | "UPSERT"                 // 存在则更新、不存在则插入(幂等)
//   | "INSERT"                 // 无条件插入
//   | "INSERT_IF_ABSENT"       // 按 key 判断,不存在才插入
//   | "UPDATE"                 // 按 key 定位并更新
//   | "NON_IDEMPOTENT_UPSERT"  // 非幂等 UPSERT

⚠️ 设计预览:以上为 API 名称所表达的语义, 具体边界(如 UPDATE 遇到不存在的 key)待上游发布后以实际行为为准。

一个输入、五种模式

以「书」为例,输入只给更新所需的字段,key 标记 id

// ② 输入 DTO:id 标记为保存键,其余为可写字段
import { model, prop, dto } from '@ts-grm/core'
import type { TypeOf } from '@ts-grm/core'

const Book = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    edition = prop.i32()
    price = prop.num(10, 2)
})

const BookInput = dto.input(Book, c => [
    c.id.key(),        // 保存键:UPSERT/UPDATE 用它定位行
    c.name,
    c.price,
])
// ③ 同一输入 × 不同 root(设计形态)
// const payload: TypeOf<BookInput> = { id: 99n, name: "…", price: 50 }
// await sqlClient.save(BookInput, payload, { root: "INSERT" })          // 无条件新增
// await sqlClient.save(BookInput, payload, { root: "INSERT_IF_ABSENT" }) // id=99 不存在才插入
// await sqlClient.save(BookInput, payload, { root: "UPDATE" })           // 按 id=99 更新
// await sqlClient.save(BookInput, payload, { root: "UPSERT" })           // 有则更、无则增
// await sqlClient.save(BookInput, payload, { root: "NON_IDEMPOTENT_UPSERT" })

差异集中在幂等性key 的参与方式

  • INSERT:不关心 key,直接插入——重复提交会产生重复行;
  • INSERT_IF_ABSENT:先按 key 探测,不存在才插入(「有则跳过」);
  • UPDATE:按 key 定位并更新,不存在的 key 不产生插入;
  • UPSERT:按 key 合并——存在走更新、不存在走插入,可安全重放(幂等);
  • NON_IDEMPOTENT_UPSERT:语义同 UPSERT,但每次执行都实际写(不因数据未变化而跳过)。

key():定位依据不只主键

key() 不限于 id。标量与 embedded 都可以标记, 按业务唯一键定位是常见用法——例如书的「名称 + 版次」在书店语境里唯一:

// ④ key() 支持业务唯一键:name + edition 作为定位依据
const BookInputByKey = dto.input(Book, c => [
    c.name.key(),
    c.edition.key(),
    c.price,
])
// save(BookInputByKey, { name: "TypeScript in Depth", edition: 2, price: 79 },
//      { root: "UPSERT" })   // 命中 (name, edition) → 更新价;未命中 → 插入

肉眼可辨识的收益:「存在并更新、不存在则创建」不再需要先查一次——把 UPSERT + key 交给 save,由一条语句完成。

与关联载荷的关系

root 只管根对象那一行。输入里内嵌的关联(.with() / $ref() 覆盖到的部分)由第二档选项 associated 按路径逐个约束——见下一页「关联保存模式」。