跨表操作必须使用同一事务实例,Transaction()不支持跨库和局部回滚;多表协同需手动管理SavePoint;JOIN分页须隔离Session防Count失真;跨库原子性需业务补偿而非事务。

跨表操作必须用同一个 *gorm.DB 事务实例,不能靠多个独立 db 变量拼凑原子性;GORM 的 Transaction() 是闭环,不支持跨库、不支持局部回滚,真要多表协同又允许部分失败,得手动管理 SavePoint。
用 Transaction() 包裹所有跨表写入
这是最常见也最容易出错的起点:很多人以为调一次 Transaction() 就能自动把所有后续 Create() 都拉进事务——但前提是这些操作必须都发生在传入回调里的那个 tx 实例上。
- 错误写法:
db.Transaction(func(tx *gorm.DB) error { tx.Create(&u); db.Create(&p) })—— 第二个db.Create完全游离在事务外 - 正确写法:所有模型操作都显式调用
tx.Create()、tx.Save()、tx.Model().Updates() - 别在事务里混用
db.First()或db.Where().Find(),它们走的是普通连接池,不受事务上下文约束 - 如果涉及关联结构体(如
User带Profile),且想用 GORM 自动处理外键和级联,确保Profile字段有正确 tag:gorm:"foreignKey:UserID"
多表关联插入失败时,只回滚子操作不丢主记录
Transaction() 一旦返回 error 就全盘回滚,没法“用户建成功,但 profile 校验失败只撤 profile”。这时候必须跳出封装,用 SavePoint。
- GORM 的
tx.SavePoint("sp_name")实际执行的是SAVEPOINT sp_name,不是新开事务 - 后续失败时调
tx.RollbackTo("sp_name"),只撤销该点之后的操作,tx.Commit()仍可提交前面的内容 - 名字必须唯一,建议带业务前缀:
"sp_profile_upsert",避免并发请求覆盖同一保存点 - 别依赖
tx.Commit()自动清理保存点——PostgreSQL 要求你显式RELEASE SAVEPOINT或等事务结束才释放,但 GORM 不提供ReleaseSavePoint方法,所以通常不 release 也没问题,只要事务最终 commit/rollback
JOIN 查询分页时,Count 和 Find 必须隔离 Session
跨表 JOIN 分页最常踩的坑是总数不准:直接链式调用 Where().Joins().Count() 和 Limit().Offset().Find(),Count() 会偷偷复用前面的 LIMIT 和 OFFSET,导致 total 永远 ≤ 当前页条数。
- 正确姿势:
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Joins(...).Count(&total) - 复杂 JOIN(含子查询、ON 条件带函数)建议手写子查询:
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u LEFT JOIN profiles p ON u.id = p.user_id WHERE u.status = ?) t", status).Scan(&total) - 分页结果不能用
Find(&[]User{}),因为 JOIN 后字段不匹配模型定义;改用Select().Scan(&[]struct{...}),字段名和别名严格对齐 - 务必显式
Order(),且排序字段要有联合索引,否则 MySQL 对 JOIN 结果无序承诺,翻页会漏数据或重复
多数据库之间无法共享事务,别试图用 Transaction() 协调
一个 Transaction() 只作用于调用它的那个 *gorm.DB 实例。写 userDB.Transaction(func(tx *gorm.DB) error { userDB.Create(); logDB.Create() }) 是无效的:后者完全脱离事务上下文,甚至可能连到不同物理机器。
- 跨库原子性不存在于 GORM 层,必须靠业务补偿:比如先写主库成功,再发消息异步写日志库;失败则触发告警+人工核对
- 每个库必须独立初始化:
var userDB, logDB *gorm.DB,各自gorm.Open(),不可复用同一变量 - 若需单库内复用连接(如事务中避免新建连接),可用
userDB.Session(&gorm.Session{NewDB: true}).Begin(),但这解决不了跨库问题
真正难的从来不是语法怎么写,而是判断哪一步该进事务、哪一步该设保存点、哪一步根本不能放进去——边界模糊时,宁可拆成多个小事务 + 幂等 + 补偿,也别强塞进一个大 Transaction() 里硬扛。


















