必须接收 gorm.db,不能接收 sql.tx 或泛型接口;因gorm事务本质是带上下文的*gorm.db,所有链式操作(如preload、create)依赖其类型,且仓储只执行数据操作,事务生命周期须由用例层统一控制。

仓储层接收 *gorm.DB 还是自定义事务类型?
必须接收 *gorm.DB,不能接收 *sql.Tx 或泛型接口。GORM 的事务对象本质就是带上下文的 *gorm.DB 实例,所有链式操作(Create、Where、Preload)都依赖它。仓储方法签名应为:func (r *userRepo) Create(ctx context.Context, tx *gorm.DB, user *User) error。传入 *sql.Tx 会导致无法调用 tx.Preload() 等 GORM 特有方法;用空接口或泛型包装则破坏类型安全,Wire 也无法正确注入。
为什么不能在仓储里调 db.Transaction?
因为事务生命周期必须由用例层(如 HTTP handler 或领域服务)控制。仓储只负责执行单次数据操作,不决定“是否开启”或“是否提交”。否则会出现:
- 嵌套事务失控:上层已开启
tx := db.Begin(),仓储内再调db.Transaction(...)会新建独立事务,绕过外层一致性保障 - 连接泄漏:
Transaction()内部 defer 回滚,但若仓储被多次复用,可能重复开启/未关闭 - 测试困难:单元测试需 mock 整个事务行为,而非只 mock 数据操作
正确做法是把事务实例作为参数显式传入,让上层统一管理 commit/rollback。
事务传参时如何避免混用 db 和 tx?
常见错误是仓储内部仍调用全局 db.Create(),导致部分写入绕过事务。必须确保:
- 所有 CRUD 操作都走传入的
tx参数,例如tx.Create(user),而非r.db.Create(user) - 预加载也必须用
tx.Preload("Profile").First(&u),否则关联查询走的是无事务的默认连接 - 若仓储内部需调其他仓储方法,同样要把
tx向下透传,不能在中间层重新取r.db
漏掉任意一处,原子性就失效——比如订单创建成功,但库存扣减走了主连接,已落库不可逆。
事务超时或 panic 时,仓储层要做什么?
仓储层不处理回滚,也不 recover panic。它的职责边界很窄:只执行 SQL 并返回 error。回滚逻辑必须留在上层:
- 用
db.Transaction()时,panic 会被自动捕获并回滚,但 error 必须显式 return,否则事务静默丢弃 - 用
tx := db.Begin()手动管理时,仓储返回 error 后,上层需判断并调tx.Rollback();绝不能在仓储里 defertx.Rollback(),否则成功时也会误回滚 - 任何 HTTP 调用、日志写入、缓存更新都严禁放在仓储方法内——它们不属于数据契约,且会拖垮事务时间
最易被忽略的一点:事务对象不可复用。一旦 tx.Commit() 或 tx.Rollback() 被调用,该 tx 实例立即失效,后续再调 tx.Create() 会 panic 报 "invalid transaction"。











