--- tags: [GORM, Go, ORM, 钩子函数, Callback, BeforeCreate, AfterUpdate, Lifecycle] create time: 2026-04-28 00:00 --- # 钩子函数 ## 概述 钩子(Hook)是 GORM 生命周期回调机制,允许你在特定操作前后注入自定义逻辑。它们是「横切关注点」的理想载体——不需要在每个业务代码里重复写加密、日志、审计等通用逻辑。 ```mermaid flowchart TD subgraph CREATE["🟢 Create"] A[Create 调用] --> BC["BeforeCreate"] BC --> CreateSQL["执行 INSERT"] CreateSQL --> AC["AfterCreate"] end subgraph UPDATE["🟡 Update"] D[Update 调用] --> BU["BeforeUpdate"] BU --> UpdateSQL["执行 UPDATE"] UpdateSQL --> AU["AfterUpdate"] end subgraph DELETE["🔴 Delete"] E[Delete 调用] --> BD["BeforeDelete"] BD --> DeleteSQL["执行 DELETE"] DeleteSQL --> AD["AfterDelete"] end subgraph FIND["🔵 Find/Query"] F[Find/First 调用] --> AF["AfterFind"] end style CREATE fill:#22C55E,color:#fff,stroke:#16A34A style UPDATE fill:#EAB308,color:#000,stroke:#CA8A04 style DELETE fill:#EF4444,color:#fff,stroke:#DC2626 style FIND fill:#3B82F6,color:#fff,stroke:#2563EB style BC fill:#22C55E,color:#fff style AC fill:#22C55E,color:#fff style BU fill:#EAB308,color:#000 style AU fill:#EAB308,color:#000 style BD fill:#EF4444,color:#fff style AD 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 } ``` > [!tip] Changed vs Updated > - `tx.Statement.Changed("field")`:只要调用过 `Updates(map)` 并包含该字段,就返回 true(无论新旧值是否相同) > - `tx.Statement.Updated("field")`:返回值与 Changed 一致,但额外校验「新值 ≠ 旧值」 > - 如果直接通过结构体赋值再调 `Save()` / `UpdateColumn()`,这些方法都**不会追踪变化**,需用 `Updates` 才有效 ### 删除阶段 ```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)` | 字段是否被更新且值发生变化 | ### Model 级 vs 全局 Callback GORM 支持两种注册方式,优先级为 **Model 级 > 全局**: ```go // Model 级钩子——实现 gorm.LifecycleHooks 接口(最常用) type Order struct{} func (Order) BeforeCreate(tx *gorm.DB) error { ... } ``` > [!note] LifecycleHooks 接口 > GORM v2 定义了以下接口,struct 只要方法签名匹配即自动注册为钩子: > - `BeforeCreate(tx *gorm.DB) error` > - `AfterCreate(tx *gorm.DB) error` > - `BeforeUpdate(tx *gorm.DB) error` > - `AfterUpdate(tx *gorm.DB) error` > - `BeforeDelete(tx *gorm.DB) error` > - `AfterDelete(tx *gorm.DB) error` > - `AfterFind(tx *gorm.DB) error` > > 你也可以使用 `*gorm.State` 替代 `*gorm.DB`(gint-gorm 兼容模式),但在大多数场景下 `*gorm.DB` 更通用。 #### 全局 Callback —— 作用于所有模型 ```go // 在 Create SQL 执行之前注册自定义逻辑 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) } } }) // 在 Delete 之后追加逻辑(不阻塞后续 callback) db.Callback().Delete().After("gorm:delete").Register("cleanup_cache", func(tx *gorm.DB) { // 清理缓存等后置操作 }) // 替换 GORM 内置回调(谨慎使用!) db.Callback().Create().Replace("gorm:create", func(tx *gorm.DB) { // 完全接管 Create 流程 }) // 移除内置回调 db.Callback().Create().Remove("gorm:create") ``` > [!warning] 全局钩子注意事项 > 1. 注册时机必须在 `db.Open()` 之后、首次操作之前 > 2. 会影响**所有模型**——包括内置的 `gorm.Model` > 3. 按阶段排序:`Before(name)` / `After(name)` / `Register(name, fn)` / `Replace(name, fn)` / `Remove(name)` > 4. 频繁读写数据库的全局逻辑会拖慢所有模型,建议在钩子内用 `tx.Get()` 做条件过滤 ## 执行时序 ```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: 返回结果 ``` ## 实战:一个完整的业务模型 下面展示一个电商订单模型,综合运用多个钩子处理真实场景: ```go type Order struct { gorm.Model UserID uint `gorm:"not null;index"` Amount decimal.Decimal Status string // pending → paid → shipped → completed PaidAt *time.Time // 支付时间,nil = 未支付 Version int // 乐观锁版本号 CreatedBy string // 创建人标识 } func (o *Order) BeforeCreate(tx *gorm.DB) error { // ① 初始化状态和审计字段 if o.Status == "" { o.Status = "pending" } o.CreatedBy = getCurrentUserID() // 从 context 获取 o.Version = 1 return nil } func (o *Order) BeforeUpdate(tx *gorm.DB) error { // ② 版本号递增 + 状态机校验 o.Version++ validTransitions := map[string][]string{ "pending": {"paid"}, "paid": {"shipped"}, "shipped": {"completed"}, } newStatus := tx.Statement.Schema.ValueOfField(tx.Statement.Schema.FieldsByName["Status"]) allowed, ok := validTransitions[o.Status] if !ok || !contains(allowed, newStatus.String()) { return fmt.Errorf("非法状态转换: %s → %s", o.Status, newStatus) } return nil } func (o *Order) AfterUpdate(tx *gorm.DB) error { // ③ 状态变更时发送通知 if old, ok := tx.Data.(*Order); ok && old.Status != o.Status { sendNotification(o.UserID, "订单状态变更为: "+o.Status) } return nil } func (o *Order) AfterDelete(tx *gorm.DB) error { // ④ 软删除后清理缓存 redis.Del(context.Background(), "order:"+strconv.Itoa(int(o.ID))) return nil } func contains(slice []string, s string) bool { for _, v := range slice { if v == s { return true } } return false } ``` > [!question] 为什么 AfterDelete 能拿到被删的数据? > 因为 GORM 的软删除是先查再标记 `deleted_at`,AfterDelete 钩子中仍然可以通过 `tx.Statement.ReflectValue` 访问到原始对象数据。 ## 常用场景速查 | 需求 | 推荐方案 | 说明 | |------|---------|------| | 自动填充 CreatedAt / UpdatedAt | 用 `autoTime` tag 更简单 | 手动实现则放 BeforeCreate / BeforeUpdate | | 密码哈希 | BeforeCreate + BeforeUpdate(仅 Changed) | 避免重复哈希 | | 乐观锁 | BeforeUpdate(检查 version) | 并发安全的经典方案 | | 审计日志 | AfterCreate / AfterUpdate / AfterDelete | 操作完成后记录 | | 数据脱敏 | AfterFind | 对外输出前格式化(注意性能) | | 关联清理 | BeforeDelete | 删除前解除外部引用 | | 全局默认值 | Callback.Register() | 作用于所有模型,需条件过滤 | ## 最佳实践 > [!checklist] 使用钩子时的 Checklist > 1. **永远用 tx 不用 db** — 钩子内部不要调用 `db.Create()` / `db.Raw()`,必须使用传入的 `tx` 参数,否则会导致死锁 > 2. **AfterFind 中不要修改原对象** — 直接改字段会影响所有调用方;需要脱敏时在接口层另行处理 > 3. **不要在钩子里发送 HTTP 请求或做耗时 IO** — 会阻塞主流程;改用 channel + goroutine 异步处理 > 4. **Changed/Updated 只在 Updates 时有效** — Save 是全量写入、Select/Omit 不会触发追踪 > 5. **批量操作也会逐条触发** — `Create(&[]Model{})` 每条都走钩子;如想跳过用 `db.Session(&gorm.Session{SkipHooks: true}).Create(...)` > 6. **钩子返回 nil != 没返回值** — Go 中空指针 nil 不等于 error 零值,中断操作必须 `return errors.New("原因")` ## 关联笔记 - [[02-模型定义]] - [[03-CRUD 操作]] - [[08-事务管理]] - [[11-批量操作]] - [[10-字段标签]](autoTime / autoCreateTime 等内置标签)