Bun不是ORM,而是SQL构建器+运行时映射器,不自动生成SQL、不维护状态、无懒加载;需手动写查询、显式调用Scan/Exec、结构体须标注pk/array标签,JSONB用json.RawMessage,事务必须显式传递tx实例且不可复用。

Bun 不是 ORM,它不自动管理事务上下文,也不像 GORM 那样提供 Begin/Commit 方法链式调用。在 Gin 中整合 Bun 做事务,核心难点是:事务实例必须显式传递、不能复用全局 db、并发场景下容易静默失败。
为什么 db.InTx() 返回的新实例不能直接赋给全局变量
常见错误是把 tx := db.InTx(ctx, nil) 得到的 tx 存到全局或中间件 context 里,然后在 handler 里直接用 global.Tx.Model(...).Insert() —— 这会跳过事务,实际走的是底层连接池的默认连接。
-
db.InTx()返回的是带事务上下文的全新*bun.DB实例,它内部绑定了当前sql.Tx对象 - 这个实例不是线程安全的,也不能跨 goroutine 复用(比如在
errgroup里共享同一个tx) - 一旦你把它存成全局变量,后续请求可能拿到已
Commit或Rollback的事务实例,调用.Exec()会 panic 或返回sql.ErrTxDone
如何在 Gin handler 中安全启动并传递 Bun 事务
必须把事务实例作为参数显式传入业务逻辑,而不是依赖闭包或全局状态。推荐用 c.Set("tx", tx) + c.MustGet("tx").(*bun.DB),但更稳妥的是直接传参。
- 在中间件中创建事务:
tx := db.InTx(c.Request.Context(), nil) - 立刻检查是否成功:
if tx == nil { c.AbortWithStatusJSON(500, gin.H{"error": "failed to start tx"}) } - handler 函数签名改为接收
*bun.DB参数,例如:func createOrder(c *gin.Context, tx *bun.DB) - 调用前解包:
createOrder(c, c.MustGet("tx").(*bun.DB)) - 事务结束必须由中间件统一控制:成功则
tx.Close()(等价于Commit),失败则tx.Rollback()
Bun 事务里更新失败却不报错的三个典型原因
现象是 tx.Model(&u).Set("status = ?", "paid").WherePK().Exec() 返回 nil 错误,但数据库没变 —— 实际是静默跳过。
- 主键字段没打
sql:",pk"标签,WherePK()生成空 WHERE 条件,导致全表更新(或零行更新) - 结构体指针传错:
tx.Model(u)(值)而非tx.Model(&u)(地址),Scan()和Exec()都无法写入或定位 - 事务未真正提交:忘记在 defer 或 recover 后调用
tx.Close(),连接被释放但变更未持久化
PostgreSQL JSONB 和数组字段在事务中怎么正确读写
事务不影响字段映射逻辑,但 NULL 处理不当会在事务内直接 panic,中断整个流程。
-
jsonb字段必须用*json.RawMessage(指针类型),否则扫描NULL时 panic -
text[]字段需加pg:",array"标签,且 Go 类型为[]string或*[]string - 写入前手动判断是否为 nil:
if req.Meta != nil { order.Meta = *req.Meta },避免空指针解引用 - 不要在事务里用
json.Marshal直接塞字符串——Bun不识别,会当普通 text 插入,丢失 jsonb 功能
事务真正的复杂点不在开启和提交,而在于 *所有模型操作都必须使用同一个 bun.DB 实例**,且该实例生命周期必须与 HTTP 请求完全对齐。漏掉一次 .Scan()、传错一个指针、多一次 db.QueryRow 而非 tx.QueryRow,都会让事务形同虚设。


















