Skip to content
Go
钩子与中间件

钩子与中间件

GORM 提供完整的生命周期钩子(Hooks/Callbacks),允许在 CRUD 操作前后插入自定义逻辑。同时也支持自定义 Plugin 扩展框架行为。


钩子生命周期

Create:  BeforeSave → BeforeCreate → Save → AfterCreate → AfterSave
Update:  BeforeSave → BeforeUpdate → Save → AfterUpdate → AfterSave
Delete:  BeforeDelete → Delete → AfterDelete
Query:   AfterFind

每个钩子都定义为模型的方法,GORM 在执行对应操作时自动调用:

type User struct {
    gorm.Model
    Name string
    Age  int
}

// 所有写入操作前执行
func (u *User) BeforeSave(tx *gorm.DB) error {
    return nil
}

// 所有写入操作后执行
func (u *User) AfterSave(tx *gorm.DB) error {
    return nil
}

Create 钩子

type User struct {
    gorm.Model
    Name     string
    Password string
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // bcrypt 密码哈希
    hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.Password = string(hashed)
    return nil
}

func (u *User) AfterCreate(tx *gorm.DB) error {
    // 发送欢迎邮件、记录审计日志等
    log.Printf("新用户创建: ID=%d, Name=%s", u.ID, u.Name)
    return nil
}
// 钩子中返回错误 → 操作被取消
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if len(u.Name) < 2 {
        return errors.New("名称长度不足")
    }
    return nil
}

// db.Create(&User{Name: "A"}) → Error: 名称长度不足

Update 钩子

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    // 记录变更前的数据快照
    u.UpdatedAt = time.Now()
    return nil
}

func (u *User) AfterUpdate(tx *gorm.DB) error {
    // 清除缓存
    cache.Del(fmt.Sprintf("user:%d", u.ID))
    return nil
}

Delete 钩子

func (u *User) BeforeDelete(tx *gorm.DB) error {
    // 防止误删(如禁止删除管理员)
    if u.ID == 1 {
        return errors.New("禁止删除超级管理员")
    }
    return nil
}

func (u *User) AfterDelete(tx *gorm.DB) error {
    // 清理关联数据(如删除头像文件)
    if u.Avatar != "" {
        os.Remove(u.Avatar)
    }
    return nil
}

Query 钩子

func (u *User) AfterFind(tx *gorm.DB) error {
    // 数据脱敏 / 格式化
    u.Name = strings.TrimSpace(u.Name)

    // 解密敏感字段
    if u.EncryptedPhone != "" {
        phone, err := decrypt(u.EncryptedPhone)
        if err != nil {
            return err
        }
        u.Phone = phone // 解密后的手机号放到非持久化字段
    }
    return nil
}

🚨 陷阱AfterFind 会大幅度影响查询性能,尤其是在循环遍历大量记录时。避免在其中执行数据库操作或复杂计算。


钩子执行条件

// 跳过钩子
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)

// 跳过部分钩子
db.Session(&gorm.Session{
    SkipDefaultTransaction: true,
    // 单独的控制钩子类型的选项在新版 GORM 中需自行实现
}).Create(&user)

// 批量操作时,每个记录独立触发钩子
users := []User{{Name: "A"}, {Name: "B"}, {Name: "C"}}
db.Create(&users) // BeforeCreate 触发 3 次

// 批量更新不触发钩子(使用 Updates + struct 条件时)
db.Model(&User{}).Where("status = ?", "inactive").Update("status", "deleted")
// ⚠ 批量 Updates 不触发 BeforeUpdate/AfterUpdate 钩子

🚨 陷阱db.Model(&User{}).Where(...).Updates(...) 这种批量更新不会触发模型的钩子方法。如果需要钩子,逐条查询后单独更新。


可用的 Hook 方法完整列表

方法签名 触发时机
BeforeSave(*gorm.DB) error 所有写入前
AfterSave(*gorm.DB) error 所有写入后
BeforeCreate(*gorm.DB) error 插入前
AfterCreate(*gorm.DB) error 插入后
BeforeUpdate(*gorm.DB) error 更新前
AfterUpdate(*gorm.DB) error 更新后
BeforeDelete(*gorm.DB) error 删除前
AfterDelete(*gorm.DB) error 删除后
AfterFind(*gorm.DB) error 查询后(每条记录)

💡 Hook 方法必须是值接收者指针接收者且在模型上定义。GORM 通过反射检查方法签名并调用。


Plugin — 自定义插件

GORM 提供 Plugin 机制注册全局行为:

// 定义插件
type AuditPlugin struct{}

func (p *AuditPlugin) Name() string {
    return "AuditPlugin"
}

func (p *AuditPlugin) Initialize(db *gorm.DB) error {
    // 注册全局钩子
    db.Callback().Create().Before("gorm:create").Register("audit:before_create", func(db *gorm.DB) {
        if db.Statement.Schema != nil {
            log.Printf("[AUDIT] CREATE %s", db.Statement.Schema.Table)
        }
    })

    db.Callback().Update().After("gorm:update").Register("audit:after_update", func(db *gorm.DB) {
        log.Printf("[AUDIT] UPDATE affected %d row(s)", db.RowsAffected)
    })

    db.Callback().Delete().Before("gorm:delete").Register("audit:before_delete", func(db *gorm.DB) {
        log.Printf("[AUDIT] DELETE from %s", db.Statement.Table)
    })

    return nil
}

// 注册插件
db.Use(&AuditPlugin{})

Callback 注册方法

// 注册回调(可指定位置)
db.Callback().Create().Before("gorm:create").Register("my_plugin:before", callback)
db.Callback().Create().After("gorm:create").Register("my_plugin:after", callback)
db.Callback().Create().Replace("gorm:create", callback) // 完全替换

// 移除回调
db.Callback().Create().Remove("my_plugin:before")

// 查询回调
db.Callback().Query().After("gorm:query").Register("my_plugin:after_query", func(db *gorm.DB) {
    // 每次查询后执行
    log.Printf("SQL: %s", db.Statement.SQL.String())
})

实战:自动填充创建人/更新人

一种常见需求是自动填充 CreatedBy/UpdatedBy 字段。由于模型钩子无法直接获取当前用户信息,推荐使用全局 Callback + Context 传递:

// 注入当前用户到 context
type contextKey string
const UserIDKey contextKey = "currentUserID"

// 注册全局 Callback
func RegisterAutoFill(db *gorm.DB) {
    db.Callback().Create().Before("gorm:create").Register("autofill:create", func(db *gorm.DB) {
        if userID, ok := db.Statement.Context.Value(UserIDKey).(uint); ok {
            db.Statement.SetColumn("CreatedBy", userID)
            db.Statement.SetColumn("UpdatedBy", userID)
        }
    })

    db.Callback().Update().Before("gorm:update").Register("autofill:update", func(db *gorm.DB) {
        if userID, ok := db.Statement.Context.Value(UserIDKey).(uint); ok {
            db.Statement.SetColumn("UpdatedBy", userID)
        }
    })
}

// 使用时
ctx := context.WithValue(context.Background(), UserIDKey, uint(123))
db.WithContext(ctx).Create(&user)
db.WithContext(ctx).Updates(&user)

常见陷阱汇总

陷阱 说明
🚨 批量更新不触发钩子 Updates + Where 不触发模型的 BeforeUpdate/AfterUpdate
🚨 AfterFind 性能 每条记录都触发,大数据量时严重拖慢查询
🚨 钩子内 DB 操作 钩子内使用同一个 tx 操作会导致无限递归
🚨 返回值忽略 钩子返回 error 会中止操作,但需确保调用方检查 db.Error
🚨 SkipHooks 影响全局 使用 SkipDefaultTransaction: true 不跳过钩子;只有 SkipHooks: true 才跳过
🚨 Map 创建无钩子 db.Model(&User{}).Create(map[string]any{...}) 不触发钩子