Skip to content

04-事务与 Hook 钩子

事务是数据库保证数据一致性的核心机制,Hook(钩子)则提供了在数据操作前后插入自定义逻辑的能力。两者结合可以实现复杂的业务编排,例如订单创建时同时扣减库存、写入流水、记录日志等。本篇将系统讲解 GORM 的事务机制和各类 Hook 钩子的使用。

事务基础

手动事务:Begin、Commit、Rollback

手动事务是最灵活的方式,开发者完全控制事务的开始、提交和回滚。

go
package main

import (
	"errors"
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Account struct {
	ID      uint `gorm:"primaryKey"`
	Name    string
	Balance float64
}

func main() {
	db, err := gorm.Open(sqlite.Open("tx.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Account{})

	// 准备数据
	db.Create(&Account{Name: "Alice", Balance: 1000})
	db.Create(&Account{Name: "Bob", Balance: 500})

	// 转账:Alice 给 Bob 转 200
	err = transfer(db, 1, 2, 200)
	if err != nil {
		log.Fatal("转账失败:", err)
	}

	var alice, bob Account
	db.First(&alice, 1)
	db.First(&bob, 2)
	fmt.Printf("转账后: Alice=%.0f, Bob=%.0f\n", alice.Balance, bob.Balance)
}

func transfer(db *gorm.DB, fromID, toID uint, amount float64) error {
	// 开始事务
	tx := db.Begin()
	defer func() {
		// 发生 panic 时自动回滚
		if r := recover(); r != nil {
			tx.Rollback()
			panic(r)
		}
	}()

	if tx.Error != nil {
		return tx.Error
	}

	var from, to Account
	if err := tx.First(&from, fromID).Error; err != nil {
		tx.Rollback()
		return err
	}
	if err := tx.First(&to, toID).Error; err != nil {
		tx.Rollback()
		return err
	}

	// 余额不足检查
	if from.Balance < amount {
		tx.Rollback()
		return errors.New("余额不足")
	}

	// 扣款
	if err := tx.Model(&from).Update("balance", from.Balance-amount).Error; err != nil {
		tx.Rollback()
		return err
	}

	// 加款
	if err := tx.Model(&to).Update("balance", to.Balance+amount).Error; err != nil {
		tx.Rollback()
		return err
	}

	// 提交事务
	return tx.Commit().Error
}

Transaction 函数式事务

GORM 推荐使用 Transaction 方法,它自动处理提交和回滚,代码更简洁:

go
package main

import (
	"errors"
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Account struct {
	ID      uint `gorm:"primaryKey"`
	Name    string
	Balance float64
}

func main() {
	db, err := gorm.Open(sqlite.Open("tx2.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Account{})

	db.Create(&Account{Name: "Alice", Balance: 1000})
	db.Create(&Account{Name: "Bob", Balance: 500})

	// 函数式事务:自动提交或回滚
	err = db.Transaction(func(tx *gorm.DB) error {
		var from, to Account
		if err := tx.First(&from, 1).Error; err != nil {
			return err
		}
		if err := tx.First(&to, 2).Error; err != nil {
			return err
		}
		if from.Balance < 200 {
			return errors.New("余额不足")
		}
		if err := tx.Model(&from).Update("balance", from.Balance-200).Error; err != nil {
			return err // 返回错误会自动回滚
		}
		if err := tx.Model(&to).Update("balance", to.Balance+200).Error; err != nil {
			return err
		}
		return nil // 返回 nil 自动提交
	})

	if err != nil {
		log.Fatal("事务失败:", err)
	}

	var alice, bob Account
	db.First(&alice, 1)
	db.First(&bob, 2)
	fmt.Printf("转账后: Alice=%.0f, Bob=%.0f\n", alice.Balance, bob.Balance)
}

自动事务 vs 手动事务

GORM 默认情况下,所有写操作(Create、Update、Delete)都自动包裹在事务中。这意味着如果操作过程中出错,已修改的部分会自动回滚。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint `gorm:"primaryKey"`
	Name  string
	Email string `gorm:"uniqueIndex"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("auto.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 默认情况下:批量创建是原子操作
	users := []User{
		{Name: "A", Email: "a@x.com"},
		{Name: "B", Email: "b@x.com"},
		{Name: "C", Email: "a@x.com"}, // 重复 email,会导致整个操作失败
	}
	err = db.Create(&users).Error
	fmt.Printf("批量创建结果: %v(自动回滚)\n", err)

	// 验证:所有记录都没插入
	var count int64
	db.Model(&User{}).Count(&count)
	fmt.Printf("实际插入数量: %d\n", count)
}

嵌套事务:SavePoint

GORM 支持嵌套事务,通过 SavePoint 实现。在内层事务中回滚不影响外层。

go
package main

import (
	"errors"
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint `gorm:"primaryKey"`
	Name  string
	Email string `gorm:"uniqueIndex"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("nested.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 外层事务
	err = db.Transaction(func(tx *gorm.DB) error {
		tx.Create(&User{Name: "A", Email: "a@x.com"})
		fmt.Println("外层创建 A")

		// 内层嵌套事务
		innerErr := tx.Transaction(func(tx2 *gorm.DB) error {
			tx2.Create(&User{Name: "B", Email: "b@x.com"})
			fmt.Println("内层创建 B")
			// 内层故意失败
			return errors.New("内层失败,回滚 B 但保留 A")
		})
		fmt.Printf("内层事务结果: %v\n", innerErr)

		// 内层失败不影响外层,继续创建 C
		tx.Create(&User{Name: "C", Email: "c@x.com"})
		fmt.Println("外层创建 C")

		return nil // 外层提交
	})
	fmt.Printf("外层事务结果: %v\n", err)

	// 验证:A 和 C 被插入,B 被回滚
	var users []User
	db.Find(&users)
	fmt.Printf("最终用户数: %d(A 和 C,B 已回滚)\n", len(users))
}

事务配置:跳过默认事务提升性能

如果不需要单条写操作的原子性保证(例如单条 Insert 不涉及多表),可以跳过默认事务以提升性能(约 30% 提升)。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint `gorm:"primaryKey"`
	Name string
}

func main() {
	// 全局跳过默认事务
	db, err := gorm.Open(sqlite.Open("skip.db"), &gorm.Config{
		SkipDefaultTransaction: true,
	})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 单条插入不再包裹事务
	db.Create(&User{Name: "Tom"})

	// 显式事务依然可用
	err = db.Transaction(func(tx *gorm.DB) error {
		tx.Create(&User{Name: "Jerry"})
		tx.Create(&User{Name: "Spike"})
		return nil
	})
	fmt.Printf("显式事务结果: %v\n", err)

	var count int64
	db.Model(&User{}).Count(&count)
	fmt.Printf("用户数: %d\n", count)
}

GORM Hook 钩子

Hook 是在特定操作前后自动执行的函数,类似于其他框架中的"事件监听器"或"中间件"。GORM 提供了完整的 CRUD 生命周期钩子。

创建钩子:BeforeCreate、AfterCreate

go
package main

import (
	"fmt"
	"log"
	"strings"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint `gorm:"primaryKey"`
	Name      string
	Email     string
	CreatedAt time.Time
	UpdatedAt time.Time
}

// BeforeCreate 在创建记录前调用
func (u *User) BeforeCreate(tx *gorm.DB) error {
	fmt.Println("BeforeCreate 钩子执行")
	// 数据预处理:邮箱转小写
	u.Email = strings.ToLower(u.Email)
	// 数据校验
	if u.Name == "" {
		return fmt.Errorf("用户名不能为空")
	}
	return nil
}

// AfterCreate 在创建记录后调用
func (u *User) AfterCreate(tx *gorm.DB) error {
	fmt.Printf("AfterCreate 钩子执行,用户 ID=%d 已创建\n", u.ID)
	// 可以在这里发送欢迎邮件、记录日志等
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("hook.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	user := User{Name: "Tom", Email: "TOM@EXAMPLE.COM"}
	if err := db.Create(&user).Error; err != nil {
		log.Fatal(err)
	}
	fmt.Printf("创建后邮箱: %s\n", user.Email)
}

更新钩子:BeforeUpdate、AfterUpdate

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint `gorm:"primaryKey"`
	Name      string
	Email     string
	UpdatedAt time.Time
}

func (u *User) BeforeUpdate(tx *gorm.DB) error {
	fmt.Println("BeforeUpdate 钩子执行")
	// 可以在这里做数据校验
	if u.Name == "" {
		return fmt.Errorf("用户名不能为空")
	}
	// 也可以获取修改的字段
	// tx.Statement.Changed("Name")
	return nil
}

func (u *User) AfterUpdate(tx *gorm.DB) error {
	fmt.Printf("AfterUpdate 钩子执行,用户 ID=%d 已更新\n", u.ID)
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("hook2.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	user := User{Name: "Tom", Email: "tom@x.com"}
	db.Create(&user)

	// 更新会触发钩子
	user.Name = "Tom Updated"
	db.Save(&user)

	// 故意触发错误
	user.Name = ""
	err = db.Save(&user).Error
	fmt.Printf("更新空名结果: %v\n", err)
}

删除钩子:BeforeDelete、AfterDelete

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint `gorm:"primaryKey"`
	Name string
}

func (u *User) BeforeDelete(tx *gorm.DB) error {
	fmt.Printf("BeforeDelete 钩子执行,准备删除 ID=%d\n", u.ID)
	// 可以做权限检查、关联数据清理等
	if u.ID == 1 {
		return fmt.Errorf("不允许删除管理员账户")
	}
	return nil
}

func (u *User) AfterDelete(tx *gorm.DB) error {
	fmt.Printf("AfterDelete 钩子执行,ID=%d 已删除\n", u.ID)
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("hook3.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "admin"})   // ID=1
	db.Create(&User{Name: "user"})    // ID=2

	// 尝试删除管理员(ID=1):失败
	err = db.Delete(&User{}, 1).Error
	fmt.Printf("删除管理员结果: %v\n", err)

	// 删除普通用户(ID=2):成功
	err = db.Delete(&User{}, 2).Error
	fmt.Printf("删除普通用户结果: %v\n", err)
}

查询钩子:AfterFind

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint `gorm:"primaryKey"`
	Name      string
	NameUpper string `gorm:"-"` // 不映射到数据库
}

// AfterFind 在查询到记录后调用
func (u *User) AfterFind(tx *gorm.DB) error {
	// 计算字段,避免在数据库存储冗余数据
	u.NameUpper = fmt.Sprintf("【%s】", u.Name)
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("hook4.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "Tom"})

	var user User
	db.First(&user, 1)
	fmt.Printf("查询后 NameUpper: %s\n", user.NameUpper)

	var users []User
	db.Find(&users)
	for _, u := range users {
		fmt.Printf("  %s\n", u.NameUpper)
	}
}

钩子中使用事务

钩子函数接收 *gorm.DB 参数,这个参数就是当前事务的 tx,所有在钩子中的操作都在同一事务内执行。

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Order struct {
	ID         uint `gorm:"primaryKey"`
	UserID     uint
	Amount     float64
	Status     string
	CreatedAt  time.Time
}

type OrderLog struct {
	ID        uint `gorm:"primaryKey"`
	OrderID   uint
	Action    string
	CreatedAt time.Time
}

// AfterCreate 在订单创建后,在同一事务中写入日志
func (o *Order) AfterCreate(tx *gorm.DB) error {
	log := OrderLog{
		OrderID:   o.ID,
		Action:    "order_created",
		CreatedAt: time.Now(),
	}
	// 使用 tx 而非全局 db,确保在同一事务内
	return tx.Create(&log).Error
}

func main() {
	db, err := gorm.Open(sqlite.Open("hooktx.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Order{}, &OrderLog{})

	// 创建订单,钩子会自动在同事务内写日志
	err = db.Transaction(func(tx *gorm.DB) error {
		order := Order{UserID: 1, Amount: 100, Status: "paid"}
		return tx.Create(&order).Error
	})
	if err != nil {
		log.Fatal(err)
	}

	var logs []OrderLog
	db.Find(&logs)
	fmt.Printf("订单日志数: %d\n", len(logs))
}

钩子取消操作

在钩子中返回错误会取消当前操作并回滚事务:

go
package main

import (
	"fmt"
	"log"
	"strings"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint `gorm:"primaryKey"`
	Name  string
	Email string
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
	// 邮箱格式校验
	if !strings.Contains(u.Email, "@") {
		return fmt.Errorf("邮箱格式错误: %s", u.Email)
	}
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("cancel.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 正常邮箱
	err = db.Create(&User{Name: "A", Email: "a@x.com"}).Error
	fmt.Printf("合法邮箱创建: %v\n", err)

	// 非法邮箱:钩子取消操作
	err = db.Create(&User{Name: "B", Email: "invalid"}).Error
	fmt.Printf("非法邮箱创建: %v\n", err)

	var count int64
	db.Model(&User{}).Count(&count)
	fmt.Printf("用户数: %d(只有 A 被创建)\n", count)
}

钩子执行顺序

当多个钩子同时存在时,执行顺序为:

  1. BeforeSave(保存前,Create 和 Update 都触发)
  2. BeforeCreate / BeforeUpdate(创建/更新前)
  3. 执行 SQL
  4. AfterCreate / AfterUpdate(创建/更新后)
  5. AfterSave(保存后,Create 和 Update 都触发)
go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint `gorm:"primaryKey"`
	Name string
}

func (u *User) BeforeSave(tx *gorm.DB) error {
	fmt.Println("  1. BeforeSave")
	return nil
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
	fmt.Println("  2. BeforeCreate")
	return nil
}

func (u *User) AfterCreate(tx *gorm.DB) error {
	fmt.Println("  3. AfterCreate")
	return nil
}

func (u *User) AfterSave(tx *gorm.DB) error {
	fmt.Println("  4. AfterSave")
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("order.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	fmt.Println("创建用户,钩子执行顺序:")
	db.Create(&User{Name: "Tom"})
}

完整示例:订单创建流程

下面用一个完整的订单创建流程综合演示事务和钩子的结合使用:

go
package main

import (
	"errors"
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// Product 商品
type Product struct {
	ID        uint    `gorm:"primaryKey"`
	Name      string
	Price     float64
	Stock     int
	CreatedAt time.Time
}

// Order 订单
type Order struct {
	ID         uint    `gorm:"primaryKey"`
	UserID     uint
	ProductID  uint
	Quantity   int
	Amount     float64
	Status     string // pending / paid / cancelled
	CreatedAt  time.Time
	Product    Product `gorm:"foreignKey:ProductID"`
}

// OrderLog 订单操作日志
type OrderLog struct {
	ID        uint      `gorm:"primaryKey"`
	OrderID   uint
	Action    string
	Remark    string
	CreatedAt time.Time
}

// BeforeCreate 订单创建前:校验库存
func (o *Order) BeforeCreate(tx *gorm.DB) error {
	fmt.Println("[Hook] BeforeCreate: 校验订单")

	if o.Quantity <= 0 {
		return errors.New("购买数量必须大于 0")
	}

	// 查询商品
	var product Product
	if err := tx.First(&product, o.ProductID).Error; err != nil {
		return fmt.Errorf("商品不存在: %w", err)
	}

	// 校验库存
	if product.Stock < o.Quantity {
		return fmt.Errorf("库存不足: 需要 %d, 剩余 %d", o.Quantity, product.Stock)
	}

	// 计算订单金额
	o.Amount = product.Price * float64(o.Quantity)
	o.Status = "pending"
	return nil
}

// AfterCreate 订单创建后:扣减库存、写日志
func (o *Order) AfterCreate(tx *gorm.DB) error {
	fmt.Printf("[Hook] AfterCreate: 扣减库存、写日志 (订单ID=%d)\n", o.ID)

	// 扣减库存(在事务中)
	result := tx.Model(&Product{}).
		Where("id = ? AND stock >= ?", o.ProductID, o.Quantity).
		Update("stock", gorm.Expr("stock - ?", o.Quantity))
	if result.Error != nil {
		return fmt.Errorf("扣减库存失败: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		return errors.New("库存不足,扣减失败")
	}

	// 写订单日志
	logEntry := OrderLog{
		OrderID:   o.ID,
		Action:    "created",
		Remark:    fmt.Sprintf("创建订单,数量 %d,金额 %.2f", o.Quantity, o.Amount),
		CreatedAt: time.Now(),
	}
	if err := tx.Create(&logEntry).Error; err != nil {
		return fmt.Errorf("写日志失败: %w", err)
	}
	return nil
}

// CreateOrder 创建订单(事务包裹)
func CreateOrder(db *gorm.DB, userID, productID uint, quantity int) (*Order, error) {
	var order Order
	err := db.Transaction(func(tx *gorm.DB) error {
		order = Order{
			UserID:    userID,
			ProductID: productID,
			Quantity:  quantity,
		}
		// 创建订单会触发 BeforeCreate / AfterCreate 钩子
		// 钩子内的操作都在这个事务内
		return tx.Create(&order).Error
	})
	if err != nil {
		return nil, err
	}
	return &order, nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("orderflow.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Product{}, &Order{}, &OrderLog{})

	// 创建商品
	product := Product{Name: "Go 书籍", Price: 99, Stock: 10}
	db.Create(&product)
	fmt.Printf("初始库存: %d\n", product.Stock)

	// 创建订单
	order, err := CreateOrder(db, 1, product.ID, 3)
	if err != nil {
		log.Fatal("创建订单失败:", err)
	}
	fmt.Printf("\n订单创建成功: ID=%d, 金额=%.2f, 状态=%s\n",
		order.ID, order.Amount, order.Status)

	// 查询商品库存
	db.First(&product, product.ID)
	fmt.Printf("扣减后库存: %d\n", product.Stock)

	// 查询订单日志
	var logs []OrderLog
	db.Find(&logs)
	fmt.Printf("订单日志数: %d\n", len(logs))

	// 测试库存不足场景
	fmt.Println("\n--- 测试库存不足 ---")
	_, err = CreateOrder(db, 1, product.ID, 100)
	fmt.Printf("超量下单结果: %v\n", err)

	// 验证库存和订单数没变化
	db.First(&product, product.ID)
	var orderCount int64
	db.Model(&Order{}).Count(&orderCount)
	fmt.Printf("库存: %d, 订单数: %d(事务回滚,数据一致)\n",
		product.Stock, orderCount)
}

小结

本篇系统讲解了 GORM 的事务与钩子机制:

  1. 手动事务BeginCommitRollback,灵活但需要手动处理错误
  2. 函数式事务Transaction 方法自动提交/回滚,推荐使用
  3. 自动事务:默认开启,所有写操作自动包裹事务
  4. 嵌套事务:通过 SavePoint 实现,内层失败不影响外层
  5. 跳过默认事务SkipDefaultTransaction: true 提升约 30% 性能
  6. 创建钩子BeforeCreateAfterCreate,用于数据预处理和后续操作
  7. 更新钩子BeforeUpdateAfterUpdate,用于数据校验和审计
  8. 删除钩子BeforeDeleteAfterDelete,用于权限检查和关联清理
  9. 查询钩子AfterFind,用于计算字段或数据转换
  10. 钩子使用事务:钩子参数 tx 即当前事务,所有操作原子执行
  11. 钩子取消操作:返回错误会回滚整个事务
  12. 钩子执行顺序:BeforeSave → BeforeCreate → SQL → AfterCreate → AfterSave

下一篇我们将深入查询进阶,学习 Scope、子查询、原生 SQL 与复杂查询构建。