--- 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` 就当直删」这种隐式分支,避免漏传参数时静默绕过模块规则。