事务回滚不会自动发生,必须显式触发或由db.transaction()控制;db.create()失败后数据仍在,因gorm隐式事务仅保障单次操作原子性,不跨调用;需用db.transaction()闭包确保多操作原子性,返回error则自动回滚,返回nil才提交。

事务回滚不会自动发生,必须显式触发或由 db.Transaction() 的返回值控制;直接用 db.Create() 写入数据,哪怕出错也不会回滚。
为什么 db.Create() 失败后数据还在?
GORM 默认对单条 Create、Update、Delete 启用隐式事务 —— 但这个事务只包裹当前操作本身,不是你整个业务逻辑。一旦你写成:
db.Create(&user1) db.Create(&user2) // 这里 panic,user1 已入库
那 user1 就已经提交了。这不是 bug,是设计:GORM 不知道你“想把这两条当一个整体”。
- 隐式事务只保单次操作原子性,不跨调用
- 没进
Transaction()或手动Begin(),就等于裸连数据库 - MySQL 默认 autocommit=1,每条成功语句都立即落盘
db.Transaction() 是最简可靠的回滚方式
用函数闭包封装操作,任何一步返回非 nil 错误,整个事务自动回滚:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{ID: 1, Balance: 100}).Error; err != nil {
return err // 触发回滚
}
if err := tx.Create(&User{ID: 2, Balance: 200}).Error; err != nil {
return err // 触发回滚
}
// 无 return → 自动 Commit
return nil
})
if err != nil {
// 这里拿到的是回滚后的错误,数据全无
}
- 闭包内必须全程用
tx,不能混用db - 闭包返回
nil才提交;返回任意error都回滚 - 无需
defer tx.Rollback(),GORM 内部已处理
手动 Begin() / Commit() / Rollback() 的坑
适合需要精细控制(比如中间查状态、分步确认)的场景,但极易漏掉 Rollback():
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
if err := tx.Create(&u1).Error; err != nil {
tx.Rollback() // 必须写!否则连接卡在 open 状态
return err
}
if err := tx.Create(&u2).Error; err != nil {
tx.Rollback() // 每个分支都要写
return err
}
return tx.Commit().Error // Commit 也可能失败,得检查
-
tx.Rollback()必须显式调用,且不能只写一次 —— 每个错误分支都得覆盖 -
tx.Commit()本身可能失败(如锁超时),返回值必须检查 - 忘记
Rollback()会导致连接泄漏,后续请求卡住
嵌套事务和 SavePoint 不解决根本问题
SavePoint 只能在同一事务内设子回滚点,无法绕过外层事务生命周期:
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&a) // 成功
tx.SavePoint("sp1")
tx.Create(&b) // 失败
tx.RollbackTo("sp1") // 回退到 sp1,a 还在
return nil // 整个事务仍会 Commit → a 被提交
})
-
RollbackTo()只撤销部分操作,不影响最终Commit - 它适合“局部补偿”,不是“业务级原子性”的替代方案
- 真要多阶段控制,应拆成多个独立事务 + 幂等 + 补偿逻辑,而非依赖 SavePoint
事务回滚真正生效的前提,是你让所有相关操作落在同一个 tx 实例下,并且错误路径能穿透到 Transaction() 闭包的 return。任何绕过它的写法,本质上都是在裸写数据库。











