不能直接混用原生sql.Tx与GORM的gorm.DB事务,因GORM只识别自身Begin()创建的gorm.DB实例;必须由GORM控制底层sql.Tx生命周期,db.Transaction()是最安全统一入口,手动Begin()仅适用于需隔离级别或SavePoint的场景。

不能直接把原生 *sql.Tx 和 GORM 的 *gorm.DB 事务混用——GORM 不认你手里的 *sql.Tx,它只认自己 Begin() 出来的 *gorm.DB 实例。 想统一事务上下文,必须让 GORM 控制底层 *sql.Tx 的生命周期,而不是反过来塞进去。
为什么不能传入 *sql.Tx 给 GORM?
GORM 的事务对象本质是带状态的 *gorm.DB,它内部封装了 Statement.ConnPool,并在 Begin() 时调用驱动的 BeginTx() 获取原生 *sql.Tx,再把它绑定到自己的会话里。你若自己 new 一个 *sql.Tx,GORM 没有 API 接口接收它;强行用反射或私有字段注入,会破坏连接复用、钩子执行、预编译缓存等关键机制。
常见错误现象:db.Session(&gorm.Session{...}).WithContext(ctx) 后仍走新连接、BeforeUpdate 钩子不触发、RowsAffected 返回 0、事务提交后数据未持久化。
- GORM 所有写操作(
Create/Save/Update)默认走事务内连接,前提是全程用同一个tx *gorm.DB - 原生
*sql.Tx无法参与 GORM 的嵌套事务(SavePoint)、Session 配置(如AllowGlobalUpdate)或日志链路追踪 - 混合使用会导致连接池泄漏:GORM 认为连接还在事务中,而你已手动
Commit()或Rollback()了底层*sql.Tx
db.Transaction() 是最安全的统一入口
它由 GORM 内部管理 *sql.Tx 生命周期,自动处理 panic 捕获、错误回滚、成功提交,并确保所有 ORM 操作和 Raw()/Exec() 都跑在同一个事务上下文中。
- 闭包内必须全程使用传入的
tx *gorm.DB,不能用全局db -
Raw()和Exec()必须调tx.Raw(...).Scan()或tx.Exec(...),否则脱离事务 - 返回非 nil 错误即触发
Rollback();返回nil才Commit() - 不支持自定义
sql.TxOptions(如隔离级别),如需控制,改用手动Begin()
示例:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "A"}).Error; err != nil {
return err // 自动回滚
}
var count int64
if err := tx.Raw("SELECT COUNT(*) FROM users WHERE name LIKE ?", "%A%").Row().Scan(&count); err != nil {
return err // 原生查询也受事务保护
}
return nil // 自动提交
})
需要隔离级别或 SavePoint?用 tx := db.Begin(opts)
当必须指定 &sql.TxOptions{Isolation: sql.LevelRepeatableRead},或要分段回滚(比如部分失败不中断整体流程),就得手动控制 Begin() / Commit() / Rollback()。
-
opts参数仅对非预编译模式生效;PostgreSQL 需注意驱动是否支持对应隔离级别 -
SavePoint和RollbackTo只能用字符串命名,且必须成对出现:tx.SavePoint("sp1")→tx.RollbackTo("sp1") - 每次
RollbackTo()后务必检查错误:失败意味着 savepoint 不存在或已释放,应降级为整体Rollback() - 手动事务没有 panic 恢复逻辑,需自行加
defer或recover()
示例:
tx := db.Begin(&sql.TxOptions{Isolation: sql.LevelRepeatableRead})
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Create(&Order{}).Error; err != nil {
tx.Rollback()
return err
}
sp := "sp_items"
tx.SavePoint(sp)
if err := tx.Create(&OrderItem{}).Error; err != nil {
tx.RollbackTo(sp) // 仅回滚 item 部分
}
return tx.Commit()
想复用已有 *sql.Tx?只能绕过 GORM 事务机制
极少数场景(如跨框架集成、遗留代码迁移),你已持有外部 *sql.Tx,又想用 GORM 模型结构体做映射。此时无法走 GORM 事务流,只能:
- 用
sqlx或原生database/sql+struct手动 Scan,放弃 GORM 钩子、关联、软删除等特性 - 或用
db.Session(&gorm.Session{DryRun: true})生成 SQL,再用原生*sql.Tx执行——但需自行处理参数绑定、类型转换、错误映射 - 绝对不要尝试把
*sql.Tx强转进 GORM 内部字段;GORM v2+ 已移除公开的 ConnPool 替换接口
真正统一事务的路径只有一条:让 GORM 当事务 Owner,而不是试图把它塞进别人的事务里。











