事务保存点

保存点(SavePoint)是事务中的标记点。可以回滚到保存点,而不是回滚整个事务。这提供了更精细的事务控制。

什么是保存点

事务通常只能全部提交或全部回滚。保存点允许部分回滚:

BEGIN
  操作1
  SAVEPOINT sp1
  操作2
  SAVEPOINT sp2
  操作3
ROLLBACK TO sp1  -- 回滚到 sp1,操作2和3被撤销,操作1保留
COMMIT

GORM 保存点

创建保存点:

tx := db.Begin()
tx.Create(&user)

tx.SavePoint("sp1")

tx.Create(&order)
if someCondition {
    tx.RollbackTo("sp1")
}

tx.Commit()

保存点命名

保存点需要命名,用于后续回滚:

tx.SavePoint("before_order")
tx.RollbackTo("before_order")

命名要有意义,方便理解代码意图。

部分回滚示例

func ProcessData(db *gorm.DB) error {
    tx := db.Begin()
    defer tx.Rollback()
    
    var user User
    tx.First(&user, 1)
    
    tx.SavePoint("before_update")
    
    user.Name = "新名字"
    if err := tx.Save(&user).Error; err != nil {
        tx.RollbackTo("before_update")
    }
    
    tx.SavePoint("before_order")
    
    order := Order{UserID: user.ID}
    if err := tx.Create(&order).Error; err != nil {
        tx.RollbackTo("before_order")
    }
    
    return tx.Commit().Error
}

保存点与嵌套事务

GORM 的嵌套事务内部就是用保存点实现:

db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&user)
    
    // 内层事务
    return tx.Transaction(func(tx2 *gorm.DB) error {
        // 这里实际创建了保存点
        tx2.Create(&order)
        return nil // 释放保存点
    })
})

内层事务失败会回滚到保存点,外层事务继续。

条件性回滚

根据条件决定是否回滚:

func CreateOrderWithCheck(db *gorm.DB, order Order) error {
    tx := db.Begin()
    defer tx.Rollback()
    
    tx.SavePoint("after_order")
    if err := tx.Create(&order).Error; err != nil {
        return err
    }
    
    for _, item := range order.Items {
        tx.SavePoint("before_item_" + strconv.Itoa(int(item.ID)))
        
        var product Product
        if err := tx.First(&product, item.ProductID).Error; err != nil {
            tx.RollbackTo("before_item_" + strconv.Itoa(int(item.ID)))
            continue
        }
        
        if product.Stock < item.Quantity {
            tx.RollbackTo("before_item_" + strconv.Itoa(int(item.ID)))
            continue
        }
        
        tx.Model(&product).Update("stock", gorm.Expr("stock - ?", item.Quantity))
    }
    
    return tx.Commit().Error
}

保存点管理

封装保存点操作:

type SavePoint struct {
    tx   *gorm.DB
    name string
}

func NewSavePoint(tx *gorm.DB, name string) *SavePoint {
    tx.SavePoint(name)
    return &SavePoint{tx: tx, name: name}
}

func (sp *SavePoint) Rollback() {
    sp.tx.RollbackTo(sp.name)
}

func (sp *SavePoint) Release() {
    // MySQL 不支持 RELEASE SAVEPOINT,GORM 也不提供
    // 保存点会在事务结束时自动释放
}

// 使用
func ProcessOrder(db *gorm.DB) error {
    tx := db.Begin()
    defer tx.Rollback()
    
    sp := NewSavePoint(tx, "before_process")
    
    if err := doSomething(tx); err != nil {
        sp.Rollback()
    }
    
    return tx.Commit().Error
}

实际案例

批量导入容错

func ImportUsers(db *gorm.DB, users []User) (int, error) {
    tx := db.Begin()
    defer tx.Rollback()
    
    successCount := 0
    
    for i, user := range users {
        spName := fmt.Sprintf("user_%d", i)
        tx.SavePoint(spName)
        
        if err := tx.Create(&user).Error; err != nil {
            tx.RollbackTo(spName)
            log.Printf("导入用户 %s 失败: %v", user.Name, err)
            continue
        }
        
        successCount++
    }
    
    return successCount, tx.Commit().Error
}

多步骤流程

func ProcessPayment(db *gorm.DB, payment Payment) error {
    tx := db.Begin()
    defer tx.Rollback()
    
    tx.SavePoint("start")
    
    if err := tx.Create(&payment).Error; err != nil {
        return err
    }
    
    tx.SavePoint("after_payment")
    
    var order Order
    if err := tx.First(&order, payment.OrderID).Error; err != nil {
        tx.RollbackTo("start")
        return err
    }
    
    order.Status = "paid"
    if err := tx.Save(&order).Error; err != nil {
        tx.RollbackTo("after_payment")
        return err
    }
    
    tx.SavePoint("after_order")
    
    notification := Notification{
        UserID:  order.UserID,
        Content: "订单已支付",
    }
    if err := tx.Create(&notification).Error; err != nil {
        tx.RollbackTo("after_order")
        log.Printf("通知创建失败: %v", err)
    }
    
    return tx.Commit().Error
}

注意事项

保存点数量

保存点会占用资源,不要创建过多。用完及时释放(事务提交或回滚)。

命名冲突

同名保存点会被覆盖:

tx.SavePoint("sp1")
tx.Create(&user)
tx.SavePoint("sp1") // 覆盖之前的 sp1
tx.Create(&order)
tx.RollbackTo("sp1") // 回到第二个 sp1,order 被撤销

数据库支持

主流数据库都支持保存点,但语法略有差异。GORM 做了统一封装。

小结

保存点提供了事务内的部分回滚能力。合理使用可以处理复杂业务流程,实现容错机制。注意保存点的命名和数量控制。