钩子函数

钩子(Hook)是在特定操作前后自动执行的函数。GORM 提供了完整的钩子机制,可以在创建、更新、删除、查询时插入自定义逻辑。

什么是钩子

钩子是回调函数,在数据库操作的生命周期中自动触发:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    u.UUID = uuid.New()
    return nil
}

创建用户前,自动生成 UUID。

钩子类型

GORM 支持的钩子:

钩子触发时机
BeforeCreate创建记录前
AfterCreate创建记录后
BeforeUpdate更新记录前
AfterUpdate更新记录后
BeforeDelete删除记录前
AfterDelete删除记录后
BeforeSave保存前(创建或更新)
AfterSave保存后(创建或更新)
AfterFind查询后

钩子执行顺序

创建记录:

BeforeSave -> BeforeCreate -> 创建操作 -> AfterCreate -> AfterSave

更新记录:

BeforeSave -> BeforeUpdate -> 更新操作 -> AfterUpdate -> AfterSave

删除记录:

BeforeDelete -> 删除操作 -> AfterDelete

查询记录:

查询操作 -> AfterFind

定义钩子

钩子是模型的方法:

type User struct {
    ID       uint
    Name     string
    Password string
}

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

钩子参数

钩子接收 *gorm.DB 参数,可以访问当前数据库连接:

func (u *User) AfterCreate(tx *gorm.DB) error {
    log := Log{
        Action:    "create_user",
        RecordID:  u.ID,
        CreatedAt: time.Now(),
    }
    return tx.Create(&log).Error
}

阻止操作

钩子返回错误会阻止操作:

func (u *User) BeforeDelete(tx *gorm.DB) error {
    if u.Role == "admin" {
        return errors.New("不能删除管理员")
    }
    return nil
}

跳过钩子

某些操作需要跳过钩子:

db.Session(&gorm.Session{SkipHooks: true}).Create(&user)

或者用 UpdateColumn

db.Model(&user).UpdateColumn("name", "新名字")

UpdateColumn 不触发 BeforeUpdateAfterUpdate

钩子与事务

钩子在事务中执行:

db.Transaction(func(tx *gorm.DB) error {
    // BeforeCreate 在这个事务中执行
    return tx.Create(&user).Error
})

钩子中的操作和主操作在同一事务中,失败会一起回滚。

多个钩子

同一个模型可以定义多个钩子:

func (u *User) BeforeSave(tx *gorm.DB) error {
    u.UpdatedAt = time.Now()
    return nil
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
    u.CreatedAt = time.Now()
    return nil
}

执行顺序:BeforeSave -> BeforeCreate

钩子注册

除了模型方法,还可以全局注册钩子:

func init() {
    callbacks.Register("gorm:create", func(db *gorm.DB) {
        if db.Statement.Schema != nil {
            for _, field := range db.Statement.Schema.Fields {
                if field.Name == "CreatedAt" {
                    field.Set(db.Statement.ReflectValue, time.Now())
                }
            }
        }
    })
}

这种方式更底层,一般不需要。

实际应用

密码加密

func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Password != "" {
        hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), 10)
        u.Password = string(hashed)
    }
    return nil
}

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    if u.Password != "" && !strings.HasPrefix(u.Password, "$2a$") {
        hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), 10)
        u.Password = string(hashed)
    }
    return nil
}

审计日志

func (u *User) AfterCreate(tx *gorm.DB) error {
    return tx.Create(&AuditLog{
        Table:     "users",
        Action:    "create",
        RecordID:  u.ID,
        UserID:    getCurrentUserID(tx),
        CreatedAt: time.Now(),
    }).Error
}

数据验证

func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Name == "" {
        return errors.New("用户名不能为空")
    }
    if len(u.Password) < 6 {
        return errors.New("密码长度至少6位")
    }
    return nil
}

小结

钩子是 GORM 的强大功能,可以在数据库操作中插入自定义逻辑。理解钩子的执行顺序和生命周期,合理使用能简化很多业务代码。