事务嵌套

实际开发中,多个函数可能都需要事务。一个事务调用另一个事务,就形成了嵌套。GORM 通过 SavePoint 实现嵌套事务效果。

嵌套事务的问题

数据库本身不支持真正的嵌套事务。外层事务和内层事务在同一个连接上,内层事务的 Commit 不会真正提交,只是结束自己的事务范围。

func OuterFunc() error {
    tx := db.Begin()
    defer tx.Rollback()
    
    tx.Create(&user)
    
    // 内层事务
    tx2 := tx.Begin() // 问题:tx 已经是事务了
    tx2.Create(&order)
    tx2.Commit()
    
    return tx.Commit().Error
}

这种写法在 GORM 中会有问题。

GORM 的嵌套事务

GORM 用 SavePoint 模拟嵌套事务:

db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&user)
    
    return tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&order)
        return nil
    })
})

内层 Transaction 会创建保存点,而不是真正的事务。内层失败回滚到保存点,外层失败回滚整个事务。

手动嵌套

手动实现嵌套:

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

savePoint := "sp1"
tx.SavePoint(savePoint)

if err := tx.Create(&order).Error; err != nil {
    tx.RollbackTo(savePoint)
}

tx.Commit()

嵌套事务示例

订单与库存

func CreateOrder(db *gorm.DB, order Order) error {
    return db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(&order).Error; err != nil {
            return err
        }
        
        for _, item := range order.Items {
            if err := DeductStock(tx, item.ProductID, item.Quantity); err != nil {
                return err
            }
        }
        
        return nil
    })
}

func DeductStock(tx *gorm.DB, productID uint, quantity int) error {
    return tx.Transaction(func(tx2 *gorm.DB) error {
        var product Product
        if err := tx2.First(&product, productID).Error; err != nil {
            return err
        }
        
        if product.Stock < quantity {
            return errors.New("库存不足")
        }
        
        return tx2.Model(&product).Update("stock", gorm.Expr("stock - ?", quantity)).Error
    })
}

DeductStock 可以独立使用,也可以在事务中被调用。

事务传播模式

虽然 GORM 没有内置的事务传播,但可以实现类似效果:

支持外部事务

func CreateOrder(db *gorm.DB, order Order) error {
    var tx *gorm.DB
    
    if db != nil {
        tx = db
    } else {
        tx = db.Begin()
        defer func() {
            if r := recover(); r != nil {
                tx.Rollback()
            }
        }()
    }
    
    if err := tx.Create(&order).Error; err != nil {
        if db == nil {
            tx.Rollback()
        }
        return err
    }
    
    if db == nil {
        return tx.Commit().Error
    }
    return nil
}

更优雅的方式:

func WithTransaction(db *gorm.DB, fn func(tx *gorm.DB) error) error {
    if isInTransaction(db) {
        return fn(db)
    }
    return db.Transaction(fn)
}

func isInTransaction(db *gorm.DB) bool {
    return db.Statement != nil && db.Statement.ConnPool != nil
}

使用:

func CreateOrder(db *gorm.DB, order Order) error {
    return WithTransaction(db, func(tx *gorm.DB) error {
        if err := tx.Create(&order).Error; err != nil {
            return err
        }
        return CreateOrderItems(tx, order.Items)
    })
}

嵌套事务的限制

不是真正的事务

内层事务的 Commit 只是释放保存点,数据没有真正提交。

回滚范围

内层事务回滚只回到保存点,外层事务继续执行。

性能影响

每层嵌套都会创建保存点,有一定开销。

实际案例

复杂业务流程

func ProcessOrder(db *gorm.DB, order Order) error {
    return db.Transaction(func(tx *gorm.DB) error {
        if err := CreateOrder(tx, order); err != nil {
            return err
        }
        
        if err := DeductInventory(tx, order); err != nil {
            return err
        }
        
        if err := CreatePayment(tx, order); err != nil {
            return err
        }
        
        if err := SendNotification(tx, order); err != nil {
            // 通知失败不影响订单
            log.Printf("通知失败: %v", err)
        }
        
        return nil
    })
}

func CreateOrder(tx *gorm.DB, order Order) error {
    return tx.Transaction(func(tx2 *gorm.DB) error {
        return tx2.Create(&order).Error
    })
}

func DeductInventory(tx *gorm.DB, order Order) error {
    return tx.Transaction(func(tx2 *gorm.DB) error {
        for _, item := range order.Items {
            if err := tx2.Model(&Product{}).
                Where("id = ? AND stock >= ?", item.ProductID, item.Quantity).
                Update("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
                return err
            }
        }
        return nil
    })
}

小结

GORM 通过 SavePoint 实现嵌套事务效果。理解其原理,合理使用事务传播模式,能让代码更灵活。