事务中应避免使用Save,因其会全字段更新(含零值),正确做法是用Update(单字段)或Updates(多字段)配合Select精确控制字段,确保UpdatedAt等自动字段行为可控。

事务中用 Update 或 Updates 指定字段,别用 Save
在事务里执行部分更新,核心是避免 Save ——它会把整个结构体所有字段(包括零值)全刷进数据库,哪怕你只改了一个 Name,Age: 0 也会覆盖掉原来存的 25。
正确做法是:显式调用 Update(单字段)或 Updates(多字段),并确保它们运行在同一个 *gorm.DB 事务对象上:
tx := db.Begin()
if err := tx.Error; err != nil {
return err
}
defer func() {
if r := recover(); r != nil || tx.Error != nil {
tx.Rollback()
}
}()
var user User
if err := tx.First(&user, 1).Error; err != nil {
return err
}
// ✅ 正确:只更新 name 字段,age 不变
if err := tx.Model(&user).Update("name", "张三").Error; err != nil {
return err
}
// ✅ 正确:用 map 更新多个非零字段(age=0 不会被设)
if err := tx.Model(&user).Updates(map[string]interface{}{"name": "李四", "age": 30}).Error; err != nil {
return err
}
return tx.Commit().Error
-
Update和Updates在事务中行为与非事务一致,只是作用域被限制在tx对象内 - 若用
struct调用Updates(如Updates(user)),GORM 默认跳过零值字段;但map[string]interface{}会照单全收,"age": 0也会写入 - 事务中不建议混用
Save和Update,容易因字段覆盖逻辑不一致导致数据错乱
Select 配合 Save 实现字段白名单式更新
如果你确实需要 Save 的“主键判断插入/更新”能力,又想控制字段范围,Select 是唯一可靠方式。它会强制 GORM 只生成包含指定字段的 SQL:
tx := db.Begin()
// ...
user := User{ID: 1, Name: "王五", Age: 0} // Age=0 是故意的
// ✅ 只让 Save 更新 name 字段,忽略 Age、UpdatedAt 等其他任何字段
if err := tx.Select("name").Save(&user).Error; err != nil {
tx.Rollback()
return err
}
-
Select必须在Save前链式调用,且只对紧邻的下一次Save生效 - 字段名必须和数据库列名一致(区分大小写,取决于驱动配置),不能写结构体字段名如
Name - 不支持嵌套字段或表达式,比如
Select("name, updated_at")可以,Select("COALESCE(name, '未知')")不行
注意 UpdatedAt 字段的自动行为
GORM 默认会在 Update 和 Updates 时自动更新 UpdatedAt 字段(如果模型定义了该字段)。但在事务中,这个行为有时会干扰预期:
- 如果你手动设置了
UpdatedAt值(比如从客户端传入),GORM 仍会覆盖它 —— 这是默认行为,无法关闭 - 若需跳过自动更新,得用
Select显式排除:tx.Model(&user).Select("name").Update("name", "新名") - 使用
Save+Select时,UpdatedAt不会自动更新,除非你把它写进Select列表里
这点容易被忽略:你以为只改了 name,结果 updated_at 被悄悄重置了,排查时得盯住 SQL 日志看实际执行语句。
子查询更新不能直接用于事务字段控制
有人尝试用子查询做动态赋值,比如 Update("status", db.Table("logs").Select("status").Where("user_id = users.id").Order("created_at DESC").Limit(1))。这种写法在事务中能跑通,但它解决的是“值来源”,不是“字段控制”:
- 它依然属于
Update调用,字段粒度由方法本身决定,不是用来替代Select或过滤字段的 - 子查询更新在事务中性能开销更大,尤其涉及 JOIN 或聚合时,容易成为瓶颈
- MySQL 8.0+ 支持 UPDATE + FROM 语法,但 GORM 不原生适配,强行用
Session或原生 SQL 会失去 ORM 安全性
真正要精确控字段,回到 Update/Updates + Select 组合,别绕远路。


















