事务中必须用tx.Raw()而非db.Raw(),因GORM事务依赖连接上下文,db.Raw()走默认连接池,脱离事务控制;混用会导致原子性破坏、静默失败或panic。

事务中必须用 tx.Raw(),不能用全局 db
原生 SQL 在事务里失效,八成是因为用了外部的 db 实例。GORM 的事务是基于连接上下文的,db.Raw() 走的是默认连接池,完全不参与当前事务。只有 tx(即 *gorm.DB 类型的事务对象)调用的 Raw() 才会复用同一底层连接。
常见错误写法:db.Raw("UPDATE users SET status = ? WHERE id = ?", "done", 1001).Exec()——这句在 db.Transaction() 回调里执行,但实际跑在另一个连接上,提交或回滚时它早已脱离控制。
- 正确入口:事务回调函数参数传入的
tx,必须全程使用它,包括tx.Create()、tx.Raw()、tx.Exec() - 别混用:不要在同一个事务里一半用
tx,一半又切回db;哪怕只有一句原生 SQL 没走tx,整个原子性就破了 - 验证是否生效:可在 SQL 中故意写错字段名(如
SET statu = ?),看报错是否出现在事务回滚前——如果报错后事务仍继续往下走,说明那句 SQL 根本没进事务
tx.Raw() 后该用 .Exec() 还是 .Scan()?
取决于 SQL 是否返回结果集。这点和普通 db.Raw() 一样,但在事务里更关键:选错会导致静默失败或 panic。
- INSERT/UPDATE/DELETE(无结果集)→ 必须用
tx.Raw(sql, args...).Exec(),然后检查err和result.RowsAffected() - SELECT 或带
RETURNING/LAST_INSERT_ID()的语句 → 必须用tx.Raw(sql, args...).Scan(&v),否则拿不到数据 - PostgreSQL 的
UPDATE ... RETURNING id不能用Exec(),得配Scan();MySQL 的INSERT主键需调result.LastInsertId(),也得从Exec()返回值取
事务内混合 ORM 和原生 SQL,钩子和自动字段会失效
tx.Raw() 是纯 SQL 通道,GORM 的生命周期钩子(BeforeUpdate、UpdatedAt)、软删除标记(DeletedAt)、字段默认值(default tag)全部不触发。这不是 bug,是设计使然。
- 如果你依赖
UpdatedAt自动更新,就不能用tx.Raw("UPDATE ..."),得改用tx.Model(&u).Updates(map[string]interface{}{"status": "done"}) - 软删除表(含
DeletedAt)上执行tx.Raw("DELETE FROM ...")是真删,不是软删;要用tx.Delete(&u)才走 GORM 删除逻辑 - 想保留部分 ORM 行为?考虑
tx.Table("users").Where("id = ?", 1001).Update("status", "done"),它生成原生 SQL 但保留钩子
参数拼接和注入风险在事务里更危险
事务常用于资金、库存等敏感操作,一旦因字符串拼接引入 SQL 注入,后果是批量脏写,且无法靠回滚补救(因为注入可能已触发其他副作用)。
- 表名、列名、ORDER BY 字段等不能用占位符,必须拼接 → 务必白名单校验,比如
if !validSortField(sort) { return err } - 禁止
"WHERE name = '" + name + "'",哪怕在事务里也一样高危;所有用户输入一律走?或sql.Named() - SQLite 驱动对命名参数(
:name)支持不稳定,生产环境优先统一用?占位符,靠顺序保证安全
Transaction() 就万事大吉”,其实只要有一处漏掉 tx 前缀,或者误用 Scan() / Exec(),整条事务链就等于裸奔。


















