--- tags: [GORM, Go, ORM, 钩子函数, Callback, BeforeCreate, AfterUpdate, Lifecycle] create time: 2026-04-28 00:00 --- # 钩子函数 ## 概述 钩子(Hook)是 GORM 生命周期回调机制,允许你在特定操作前后注入自定义逻辑。它们是「横切关注点」的理想载体——不需要在每个业务代码里重复写加密、日志、审计等通用逻辑。 ```mermaid 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 执行 → 钩子(后置)`。前置钩子返回错误可以**中断整个操作**。 ## 完整钩子列表 ### 创建阶段 ```go 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 } ``` ### 更新阶段 ```go 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 } ``` ### 删除阶段 ```go 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 } ``` ### 查询阶段 ```go 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` 判断当前操作类型: ```go 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 级 > 全局**: ```go // 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`——使用时需格外小心。 ## 钩子执行链示意图 ```mermaid 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 | 删除前解除外部引用 | ## 关联笔记 - [[02-模型定义]] - [[03-CRUD 操作]] - [[08-事务管理]] - [[11-批量操作]]