---
title: 数据写入
description: saveobjt / delete,含 $original、$expr 与 guards
pageClass: g3-api
---
# 数据写入
## `POST` `/data/saveobjt`
请求体是一个**数组**:一次可提交多个表的增、改、删,**全部在同一个事务里** —— 任何一项失败整批回滚,不会留下半套数据。
### 请求字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `table` | string | 是 | 保存目标表 |
| `key_field` | string | 是 | 主键列名,用于定位要改/删的行 |
| `updates` | object[] | | 要更新的行,主键值必须在行里 |
| `inserts` | object[] | | 要新增的行 |
| `deletes` | object[] | | 要删除的行(用 `key_field` 定位) |
| `delete_target` | string | | 声明 `deletes` 走哪条路径:`module`(按模块配置联动删子表,需 `moduleCode`)或 `direct`(直接删,需 `table` / `key_field`) |
| `guards` | object | | 前置条件,见下 |
```json
[
{
"table": "b_othercompany",
"key_field": "b_id",
"updates": [{ "b_id": "C001", "b_name": "新名字" }],
"inserts": [{ "b_id": "C002", "b_name": "新公司" }],
"deletes": [{ "b_id": "C003" }]
}
]
```
成功时 `data` 为 `null`,`message` 为 `保存成功`:
```json
{
"code": 0,
"message": "保存成功",
"data": null
}
```
> **没传 `delete_target` 时,`deletes` 是逐行按主键直删**,不会触发模块配置里的联动删除。需要联动就把 `delete_target` 声明成 `module` 并带上 `moduleCode`。
### 行内的两个保留键
键名以 `$` 开头的不会被当成业务列:
| 键 | 用途 | 说明 |
|---|---|---|
| `$original` | 乐观锁:声明「我改动之前看到的值」 | 这些字段**只进 `WHERE` 不进 `SET`**;对不上就是 `2003`。值为 `null` 时生成 `IS NULL` |
| `$expr` | 字段自增:`{"$expr": "b_amount + 5"}` | 只支持「本表列 ± 数值常量」,列必须是数值类型 |
```json
{
"b_id": "C001",
"b_amount": { "$expr": "b_amount + 5" },
"$original": { "b_name": "旧名字", "b_row_version": 3 }
}
```
- `$original` 里带 `b_row_version` 时走**整行版本锁**:服务端校验通过后自动 `+1`,请求里同名的普通值不生效(避免 `SET` 里出现两次该列)。
- `inserts` 的行**不允许**带 `$original` / `$expr` —— 新增的行没有原值可比,带了会直接报 `1000`。
- `$expr` 的字段不存在、不可写、不是数值类型、格式不对,都会在**任何一行写入之前**报 `1000`(这类参数错误不会等到写到第 N 行才暴露)。
### guards:写入前置条件
让「判断」和「写入」发生在**同一条 SQL** 里,避免先查后写的竞态。
```json
{
"table": "b_othercompany",
"key_field": "b_id",
"updates": [{ "b_id": "C001", "b_name": "新名字" }],
"guards": {
"updates": [
{
"condition": "{b_state} = 'draft'",
"refs": ["b_othercompany"],
"message": "只有草稿状态才能改名",
"retryable": false
}
]
}
}
```
`guards` 下的三个分节键按动作区分 —— `inserts` / `updates` / **`deletes`**(注意是 `deletes`):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `condition` | string | 是 | SQL 布尔表达式;`{列名}` 是受控占位符,替换为 `t.[列名]`;**不允许注释** |
| `refs` | string[] | | 声明引用的表,只进日志 |
| `message` | string | | 条件不成立时给用户看的文案 |
| `retryable` | boolean | | 默认 `true`;`false` 时前端文案不追加「请刷新后重试」 |
条件不成立时返回 `2002`,`message` 就是上面写的 `message`,`data` 带 `table` / `action` / `index`。
两条互斥规则(同时用会报 `1000`):
- `guards.deletes` 与 `delete_target` 互斥;
- `guards.deletes` 与 `dependent_deletes` 互斥。
原因是带条件的删除只能逐行唯一定位,而 `delete_target` / 依赖删除那两条旁路拿不到 guards —— 与其让条件被静默忽略,不如直接拒绝。
### 三个错误码怎么分
| code | 含义 | 典型原因 | `data` 里的定位信息 |
|---|---|---|---|
| `2001` | 保存失败 | 字段不存在、类型不匹配、数据库约束冲突等 | `table` / `action` / `index` / `cause` |
| `2002` | 前置条件不成立 | `guards` 里的条件没满足 | `table` / `action` / `index` |
| `2003` | 并发编辑冲突 | `$original` 的原值已被别人改过、`b_row_version` 不匹配 | `table` / `action` / `index` / `fields` |
`2003` 的正确处理不是重试,而是:**重新拉取该行**,用 `data.fields` 标出哪些字段已被他人修改,让用户决定覆盖还是放弃。
`action` 的取值是 `update` / `insert` / `delete`,`index` 是请求数组里的下标(从 `0` 开始)—— 批量保存失败时靠它定位到具体哪一行。
## `POST` `/data/delete`
独立删除接口。和 `saveobjt` 的区别是它**不进保存事务**、也不涉及增改。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `target` | string | 是 | `module` 或 `direct`,其它值报 `1000` |
| `moduleCode` | string | 是(`target=module`) | 模块编码(`s_module.b_id`) |
| `table` | string | 是(`target=direct`) | 表名 |
| `key_field` | string | 是(`target=direct`) | 主键列名 |
| `ids` | any[] | | 要删除的主键值;`direct` 下为空数组会直接返回 `{}` |
按模块删除(会按模块配置联动删除子表):
```json
{
"target": "module",
"moduleCode": "othercompany",
"ids": ["C001", "C002"]
}
```
指定表直接删除:
```json
{
"target": "direct",
"table": "b_othercompany",
"key_field": "b_id",
"ids": ["C001"]
}
```
```json
{
"code": 0,
"message": "操作成功",
"data": { "b_othercompany": 2 }
}
```
`data` 是 **`{表名: 实际删除行数}`** —— `direct` 只有一项;`module` 会带出所有被联动删除的表,所以可能有多项。整个删除同样在单个事务里完成。
> `target` 必须显式声明。后端**刻意不做**「没传 `moduleCode` 就当直删」这种隐式分支,避免漏传参数时静默绕过模块规则。