Appearance
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)
}钩子执行顺序
当多个钩子同时存在时,执行顺序为:
- BeforeSave(保存前,Create 和 Update 都触发)
- BeforeCreate / BeforeUpdate(创建/更新前)
- 执行 SQL
- AfterCreate / AfterUpdate(创建/更新后)
- 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 的事务与钩子机制:
- 手动事务:
Begin、Commit、Rollback,灵活但需要手动处理错误 - 函数式事务:
Transaction方法自动提交/回滚,推荐使用 - 自动事务:默认开启,所有写操作自动包裹事务
- 嵌套事务:通过 SavePoint 实现,内层失败不影响外层
- 跳过默认事务:
SkipDefaultTransaction: true提升约 30% 性能 - 创建钩子:
BeforeCreate、AfterCreate,用于数据预处理和后续操作 - 更新钩子:
BeforeUpdate、AfterUpdate,用于数据校验和审计 - 删除钩子:
BeforeDelete、AfterDelete,用于权限检查和关联清理 - 查询钩子:
AfterFind,用于计算字段或数据转换 - 钩子使用事务:钩子参数
tx即当前事务,所有操作原子执行 - 钩子取消操作:返回错误会回滚整个事务
- 钩子执行顺序:BeforeSave → BeforeCreate → SQL → AfterCreate → AfterSave
下一篇我们将深入查询进阶,学习 Scope、子查询、原生 SQL 与复杂查询构建。