7.1 KiB
7.1 KiB
tags, create time
| tags | create time | ||||||||
|---|---|---|---|---|---|---|---|---|---|
|
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 | 删除前解除外部引用 |