钩子与中间件
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{...}) 不触发钩子 |