事务中间件需显式注册,GORM事务不自动开启;必须配对开启与回滚,用db.Transaction()包裹并捕获error;嵌套事务用SavePoint;Body需提前读取并重置。

事务中间件必须显式注册,不挂载等于没写
定义一个 func(ctx iris.Context) 函数后,GORM 事务不会自动开启——Iris 不识别“事务中间件”这个概念,它只认 app.Use() 或路由级绑定。常见错误是写了事务逻辑却漏掉注册,结果所有请求都走默认无事务流程。
全局事务拦截用 app.Use(transactionMiddleware);若只想对 /api/* 生效,推荐 app.Any("/api/*", transactionMiddleware, handler)。注意:/health、/favicon.ico 这类路径也会被拦截,得在中间件开头加判断跳过:
if ctx.Path() == "/health" || ctx.Path() == "/favicon.ico" { ctx.Next(); return }- 别用
ctx.Method() == "GET"一刀切跳过——有些业务 GET 接口(如导出、幂等查询)也需事务保障
事务开启和回滚必须配对,ctx.StopExecution()不能少
GORM 的 db.Transaction() 是函数式调用,不是上下文绑定。你在中间件里开了事务,但 handler 抛 panic 或手动 ctx.StatusCode(500) 后,若没主动回滚,连接池里的事务会卡住甚至泄漏。
正确模式是:db.Transaction(func(tx *gorm.DB) error { ... }) 包裹 handler 执行,且必须捕获 error 并显式 ctx.StopExecution():
- handler 内部出错 →
tx.Rollback()→ctx.JSON(500, ...)→ctx.StopExecution() - handler 成功 →
tx.Commit(),然后才允许ctx.Next()继续(其实此时已无后续中间件) - 别依赖
defer tx.Rollback()—— 它无法区分成功/失败场景,容易误回滚
嵌套事务要靠 SavePoint,原生 Transaction 不支持重入
如果 handler 里又调了另一个带 db.Transaction() 的函数,外层事务会被中断,GORM 默认不支持嵌套。真需要分段控制,得用 SavePoint:
tx.SavePoint("sp1") → 执行子逻辑 → tx.RollbackTo("sp1") 或 tx.Release("sp1")。但注意:SavePoint 是数据库特性,MySQL 5.7+、PostgreSQL 支持,SQLite 不行。
- 不要在中间件里无条件开
SavePoint——增加额外 round-trip 开销 - 只在明确需要“局部回滚”的业务点(如创建订单+扣库存+发消息,消息失败不连坐扣库存)才手动加
- SavePoint 名字建议用
uuid.NewString()避免冲突,别硬编码"sp1"
Body 读取和事务不能混用,顺序错了就 panic
如果你的事务中间件还顺手做了签名校验或日志审计,大概率会提前读 ctx.Request().Body。而 GORM 的 db.Create() 等操作若涉及 JSON 解析(比如绑定 struct),会再次尝试读 Body —— 此时返回 io.EOF 或空数据,导致插入字段全为零值。
安全做法是在事务中间件最开头一次性读完:
body, _ := io.ReadAll(ctx.Request().Body)ctx.Request().Body = io.NopCloser(bytes.NewReader(body))- 再做签名验证 or 日志记录 or
db.Transaction() - 否则 handler 里
ctx.ReadJSON(&req)就会静默失败
这个顺序问题在线上很难复现,因为本地调试时 Body 可能还没被其他中间件消费,一上生产就崩。


















