savepoint 在 gorm 中并非嵌套事务,而是通过手动执行 savepoint 和 rollback to savepoint 原生 sql 模拟局部回滚;它不创建新事务,仅在当前 *gorm.db 实例上设点与回退,且仅支持回滚到指定保存点,不支持提交,名字须唯一,外层事务提交或回滚后所有保存点自动失效。

SavePoint 在 GORM 里不是“嵌套事务”,而是手动 SQL 模拟
GORM 的 SavePoint 和 RollbackTo 方法底层不创建新事务,只是在当前 *gorm.DB 实例上执行 SAVEPOINT sp_name 和 ROLLBACK TO SAVEPOINT sp_name 原生语句。很多人误以为调用 tx.SavePoint("sp1") 后就能像真正事务一样 Commit() 或 Rollback() —— 实际上它只支持回滚到该点,且必须配合显式错误处理。
关键限制:
-
tx.SavePoint("sp1")多次调用同一名字会报错(PostgreSQL/MySQL 均拒绝重复保存点名) - 若全局配置了
DisableNestedTransaction: true,SavePoint会静默失效,无任何 warning - 调用
tx.RollbackTo("sp1")后,事务仍处于活跃状态,后续操作继续生效;但不能再对已回滚的保存点做任何操作 - 外层事务一旦
Commit()或Rollback(),所有保存点自动失效,再调RollbackTo会触发current transaction is aborted
怎么安全地用 SavePoint 包裹可选失败的子操作
典型场景:用户注册成功后,尝试写入 profile、address、notification_setting 三张表,其中 address 校验失败不应导致用户记录丢失。这时应在主记录插入后、子操作前设点。
正确写法示例(GORM v1.20+):
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 主记录:不可逆
var user User
user.Name = "alice"
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
// 子操作一:profile(允许失败)
if err := tx.SavePoint("sp_profile"); err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Profile{UserID: user.ID, Bio: "..." }).Error; err != nil {
tx.RollbackTo("sp_profile") // 只丢弃 profile 相关变更
}
// 子操作二:address(也允许失败)
if err := tx.SavePoint("sp_address"); err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Address{UserID: user.ID, City: "xxx"}).Error; err != nil {
tx.RollbackTo("sp_address")
}
if err := tx.Commit().Error; err != nil {
return err
}
注意点:
- 每个
SavePoint名字必须唯一,推荐带业务前缀如"sp_profile",避免循环中动态生成时碰撞 -
RollbackTo不返回 error(PostgreSQL 允许对不存在的保存点执行该命令,仅 warning),但名字拼错就白写了 - 不要在
SavePoint块内调tx.Commit()或tx.Rollback(),会破坏外层事务完整性
为什么 db.Transaction() 里用 SavePoint 基本无效
db.Transaction() 是一次性闭环:回调函数返回非 nil error 就自动回滚整个事务,根本没机会执行你写的 RollbackTo。它内部不暴露事务实例,也不允许中途退出上下文。
换句话说:
- 你在
db.Transaction(func(tx *gorm.DB) error { ... })里调tx.SavePoint("sp")是合法的,但只要回调返回 error,GORM 就直接tx.Rollback(),前面设的点全被清空 - 想实现局部容错,必须跳出
db.Transaction(),改用db.BeginTx(ctx, &sql.TxOptions{})或db.Session(&gorm.Session{NewDB: true})手动启事务 - GORM 的
Session(&gorm.Session{NewTx: true})在已有事务中不会 panic,但实际是悄悄转成SAVEPOINT,返回的仍是原*gorm.DB实例,语义易混淆
容易被忽略的兼容性与驱动差异
不同数据库驱动对保存点的支持程度不同,GORM 层没有做抽象屏蔽:
- MySQL 不支持
RELEASE SAVEPOINT,PostgreSQL 支持但漏掉也不会出错;别把它当资源清理必需步骤 - pgx(v5+)提供更友好的
tx.Savepoint(name)方法,而标准database/sql驱动需手动tx.Exec("SAVEPOINT sp1") - GORM 的
SavePoint方法依赖底层驱动是否支持该 SQL 语法;SQLite 默认不启用SAVEPOINT,需编译时加-DSQLITE_ENABLE_SAVEPOINTS - 并发请求下,若用
uuid.NewString()[:8]生成保存点名,仍有极小概率碰撞,建议用完整 uuid 或加时间戳前缀
最稳妥的做法,是把保存点逻辑下沉到 *sql.Tx 层手动管理,绕过 GORM 封装——尤其在线上强一致性场景中,少一层抽象意味着少一个失控变量。











