This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/09-钩子函数.md
T
2026-04-28 20:23:33 +08:00

7.1 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
钩子函数
Callback
BeforeCreate
AfterUpdate
Lifecycle
2026-04-28 00:00

钩子函数

概述

钩子(Hook)是 GORM 生命周期回调机制,允许你在特定操作前后注入自定义逻辑。它们是「横切关注点」的理想载体——不需要在每个业务代码里重复写加密、日志、审计等通用逻辑。

flowchart TD
    A[Create 调用] --> BC["BeforeCreate"]
    BC --> CreateSQL["执行 INSERT"]
    CreateSQL --> AC["AfterCreate"]
    
    D[Update 调用] --> BU["BeforeUpdate"]
    BU --> UpdateSQL["执行 UPDATE"]
    UpdateSQL --> AU["AfterUpdate"]
    
    E[Delete 调用] --> BD["BeforeDelete"]
    BD --> DeleteSQL["执行 DELETE"]
    DeleteSQL --> AD["AfterDelete"]
    
    F[Find/First 调用] --> AF["AfterFind"]
    
    style BC fill:#EAB308,color:#fff
    style BU fill:#F59E0B,color:#000
    style BD fill:#EF4444,color:#fff
    style AF fill:#3B82F6,color:#fff

[!note] GORM 的操作顺序 每个操作的执行流:钩子(前置) → SQL 执行 → 钩子(后置)。前置钩子返回错误可以中断整个操作。

完整钩子列表

创建阶段

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // 在 INSERT 之前执行
    u.CreatedAt = time.Now()
    u.UpdatedAt = time.Now()
    
    // 对密码做哈希处理
    hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    u.Password = string(hashed)
    
    // 生成唯一 ID
    if u.UUID == "" {
        u.UUID = generateUUID()
    }
    return nil // 返回错误可中断插入
}

func (u *User) AfterCreate(tx *gorm.DB) error {
    // 在 INSERT 之后执行
    // 例如:发送欢迎邮件、记录审计日志
    log.Printf("用户 %s 创建成功,ID=%d", u.Name, u.ID)
    return nil
}

更新阶段

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    // 在 UPDATE 之前执行
    u.UpdatedAt = time.Now()
    
    // 检查乐观锁版本号
    var prev User
    tx.Select("version").First(&prev, u.ID)
    if prev.Version != u.Version {
        return errors.New("数据已被其他人修改,请刷新后重试")
    }
    return nil
}

func (u *User) AfterUpdate(tx *gorm.DB) error {
    // 同步缓存
    redis.Set(context.Background(), "user:"+strconv.Itoa(u.ID), u, time.Hour)
    return nil
}

删除阶段

func (u *User) BeforeDelete(tx *gorm.DB) error {
    // 清理关联数据(比如清除 Redis Session)
    sessionIDs, _ := getSessionIDsByUserID(u.ID)
    for _, id := range sessionIDs {
        redis.Del(context.Background(), "session:"+id)
    }
    return nil
}

func (u *User) AfterDelete(tx *gorm.DB) error {
    // 如果用了软删除,这里可以做真正的物理清理(如文件、附件)
    if tx.RowsAffected > 0 {
        deleteUploadedFiles(u)
    }
    return nil
}

查询阶段

func (u *User) AfterFind(tx *gorm.DB) error {
    // 在 SELECT 之后执行——每次 Find/First 都会触发
    // 适合用于敏感字段脱敏或格式化处理
    
    // 示例:邮箱脱敏
    if len(u.Email) > 4 {
        masked := u.Email[:2] + "***" + u.Email[len(u.Email)-2:]
        fmt.Println("已脱敏:", masked)
    }
    return nil
}

[!question] AfterFind 的性能影响 AfterFind 会在每次查询时对所有行都执行——如果一次查出 1000 条记录,钩子会运行 1000 次。不要在 AfterFind 中放重逻辑(如 RPC 调用)。

条件判断:根据操作类型选择行为

有时你希望同一个钩子在特定操作下才生效。可以通过 tx.Statement 判断当前操作类型:

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    // 只在 Password 被修改时才重新哈希
    if tx.Statement.Changed("Password") {
        hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
        u.Password = string(hashed)
    }
    return nil
}

[!tip] Statement 常用方法

方法 作用
Changed(field) 该字段是否在 Updates 中被修改
Select() 获取显式 SELECT 的字段列表
Omit() 获取 OMIT 的字段列表
Deleted() 是否是 Delete 操作
Updated(field) 字段是否被更新且值发生变化

全局钩子 vs Model 级钩子

GORM 支持两种注册方式,优先级为 Model 级 > 全局:

// Model 级钩子——写在 struct 的方法上(最常用)
type Order struct{}
func (Order) BeforeCreate(tx *gorm.DB) error { ... }

// 全局钩子——通过 Callback 注册,作用于所有模型
db.Callback().Create().Before("gorm:create").Register("set_created_at", func(tx *gorm.DB) {
    if v, ok := tx.Get("created_at_overwrite"); ok {
        if t, ok := v.(time.Time); ok {
            tx.Statement.SetColumn("CreatedAt", t)
        }
    }
})

// 使用场景:给所有模型统一设置默认时间(测试用)
db.Session(&gorm.Session{Context: ctx}).Create(&order)

[!warning] 全局钩子注意事项 全局钩子的注册时机必须在 db.Open() 之后、首次操作之前。而且全局钩子会影响所有模型——包括内置的 gorm.Model——使用时需格外小心。

钩子执行链示意图

sequenceDiagram
    participant App as 应用代码
    participant Hook as GORM 钩子系统
    participant DB as 数据库
    
    App->>Hook: db.Create(&user)
    Hook->>Hook: BeforeCreate
    Hook->>DB: INSERT INTO users...
    DB-->>Hook: 返回影响行数
    Hook->>Hook: AfterCreate
    Hook-->>App: 返回结果
    
    Note over Hook: Update: BeforeUpdate → SQL → AfterUpdate
    Note over Hook: Delete: BeforeDelete → SQL → AfterDelete  
    Note over Hook: Find: BeforeQuery → SQL → AfterFind

常见坑点速查

问题 原因 解决方案
钩子里调用了 db(而不是 tx) 死锁——钩子内再开事务嵌套 钩子内部全部使用 tx 参数
Changed() 在 Save 时总返回 false Save 是全量写入,不追踪变化 用 Updates(struct) 配合 Changed()
钩子返回了 nil 但想中断操作 零值 nil 不是错误 return errors.New("中断原因")
批量操作也会触发钩子 Create(&[]User{}) 每条都走钩子 如需跳过可用 SkipHooks session
AfterFind 脱敏污染了原始数据 直接修改结构体字段影响调用方 需要脱敏时在接口层处理,不要改原对象

钩子应用场景总结

场景 推荐钩子 说明
自动填充 CreatedAt / UpdatedAt autoTime tag 更简单 如果手动实现用 BeforeCreate / BeforeUpdate
密码哈希 BeforeCreate + BeforeUpdate(仅当 Changed) 避免重复哈希
乐观锁 BeforeUpdate(检查 version) 并发安全的经典方案
审计日志 AfterCreate / AfterUpdate / AfterDelete 操作完成后异步记录
数据脱敏 AfterFind 对外输出前格式化
关联清理 BeforeDelete 删除前解除外部引用

关联笔记