应使用原生SQL执行jsonb_set或JSON_SET,或传入完整序列化JSON;禁用GORM零值跳过机制,避免局部更新丢数据。

事务中用 Updates 更新 JSON 字段会丢数据
直接在事务里调用 db.Model(&u).Updates(map[string]interface{}{"attrs": newJSON}) 看似没问题,但实际只更新了整字段值——如果 attrs 是数据库的 jsonb 或 JSON 类型,GORM 不会做合并(merge),而是全量覆盖。你原来存的 {"a":1,"b":2},传入 {"c":3},结果就是只剩 {"c":3},a 和 b 永久丢失。
更危险的是:如果你用的是裸 string 或 []byte 映射 JSON 字段,Updates 还可能因零值跳过(比如传 "" 或 nil),导致字段被设为空字符串或触发 panic。
- 必须确保新值是完整、合法的 JSON 字节序列(
json.Marshal后的结果) - 不能依赖
Updates做“局部键更新”,它不支持json_set语义 - 事务内若混用
Save和Updates,要注意前者强制写零值,后者跳过零值,行为不一致
用 datatypes.JSON + 手动拼 SQL 实现原子合并更新
datatypes.JSON 是 GORM 官方推荐的 JSON 类型封装,但它本身不提供 merge 功能。想在事务中安全地“只改某个 key”,得绕过 GORM 的 Updates,用原生 SQL 配合驱动能力:
PostgreSQL 示例(jsonb_set):
tx.Exec(`UPDATE users SET attrs = jsonb_set(attrs, '{key}', ?::jsonb, true) WHERE id = ?`, newData, userID)
MySQL 示例(JSON_SET):
tx.Exec(`UPDATE users SET attrs = JSON_SET(attrs, '$.key', ?) WHERE id = ?`, newData, userID)
- 必须用
tx.Exec而非db.Exec,保证在同一个事务上下文 -
newData应为已序列化的[]byte或string,避免在 SQL 层再解析 - PostgreSQL 的
jsonb_set(..., true)表示路径不存在时自动创建;MySQL 的JSON_SET默认就创建,无需额外参数 - 别用
db.Raw().Updates()——Updates会二次处理参数,破坏 JSON 函数语义
自定义类型 + Scanner/Valuer 在事务中保持一致性
如果你坚持用结构体字段承载 JSON 内容(比如 type User struct { Attrs MyJSON } ),那必须确保该类型实现的 Scanner 和 Valuer 在事务中行为稳定:
-
Scan中遇到nil必须返回nil,不能 panic;遇到空切片(len(b)==0)或"null"字符串也应归一为nil -
Value中若字段是*MyStruct,nil指针必须返回nil(存为 SQL NULL),而非json.Marshal(nil)(产出"null"字符串) - 务必实现
MarshalJSON/UnmarshalJSON,否则事务外调用json.Marshal时可能 panic(尤其嵌套在其他 struct 里被反射调用) - 在事务中修改该字段后,不要在同事务里再用
db.First重查——GORM 缓存可能未刷新,看到旧值
为什么不用 db.Model().Select() 强制更新 JSON 字段
有人试过 db.Select("attrs").Model(&u).Updates(&u) 想绕过零值跳过,但这对 JSON 字段无效:只要 u.Attrs 是 string 或 []byte,且值为空("" 或 nil),GORM 仍会跳过;而如果是 datatypes.JSON,Select 也不改变其底层字节逻辑,依然要你传完整值。
真正起作用的只有两条路:
- 传完整、非空、已序列化的 JSON 数据(
datatypes.JSON或[]byte),靠Select强制写入 - 放弃 ORM 抽象,用
Exec调用数据库原生 JSON 函数做精准更新
动态 JSON 的“动态”二字,本质是 schema-less,GORM 的 struct 映射天然与之冲突。事务里最易被忽略的点,是把“Go 层面的结构体更新”和“数据库层面的 JSON patch”当成一回事——它们从来就不是一回事。











