钩子与回调


第八章:钩子与回调

8.1 钩子概述

GORM 钩子是在数据库操作生命周期中自动执行的回调函数,可以在创建、查询、更新、删除操作前后插入自定义逻辑。

8.2 支持的钩子

创建相关钩子

钩子 触发时机
BeforeSave 保存(创建/更新)前
BeforeCreate 创建前
AfterSave 保存后
AfterCreate 创建后

查询相关钩子

钩子 触发时机
AfterFind 查询后

更新相关钩子

钩子 触发时机
BeforeSave 保存前
BeforeUpdate 更新前
AfterSave 保存后
AfterUpdate 更新后

删除相关钩子

钩子 触发时机
BeforeDelete 删除前
AfterDelete 删除后

8.3 定义钩子

type User struct {
    ID        uint
    Name      string
    Password  string
    CreatedAt time.Time
    UpdatedAt time.Time
}

// BeforeCreate 创建前钩子
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    u.CreatedAt = time.Now()
    return
}

// AfterFind 查询后钩子
func (u *User) AfterFind(tx *gorm.DB) (err error) {
    // 解密敏感数据等
    return
}

8.4 完整钩子示例

type User struct {
    ID        uint
    Username  string
    Password  string
    Email     string
    Status    int
    CreatedAt time.Time
    UpdatedAt time.Time
}

// BeforeCreate 创建前:密码加密、设置默认值
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    // 密码加密
    if u.Password != "" {
        hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
        if err != nil {
            return err
        }
        u.Password = string(hashedPassword)
    }
    
    // 设置默认状态
    if u.Status == 0 {
        u.Status = 1
    }
    
    return
}

// BeforeUpdate 更新前:自动更新更新时间
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    u.UpdatedAt = time.Now()
    
    // 如果更新了密码,重新加密
    if tx.Statement.Changed("Password") {
        hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
        tx.Statement.SetColumn("Password", string(hashedPassword))
    }
    
    return
}

// AfterFind 查询后:脱敏处理
func (u *User) AfterFind(tx *gorm.DB) (err error) {
    // 脱敏:隐藏部分密码
    if len(u.Password) > 4 {
        u.Password = "****"
    }
    return
}

// BeforeDelete 删除前:检查是否可以删除
func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
    if u.Status == 2 {  // 假设2是管理员
        return errors.New("管理员账户不能删除")
    }
    return
}

8.5 SkipHooks 跳过钩子

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

// 跳过特定钩子
user.BeforeCreate = nil  // 不推荐

8.6 修改当前操作

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    // 修改当前语句的更新内容
    tx.Statement.SetColumn("Status", 1)
    return
}

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    // 检查字段是否改变
    if tx.Statement.Changed("Name", "Email") {
        tx.Statement.SetColumn("UpdatedAt", time.Now())
    }
    return
}

8.7 数据库事务中的钩子

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
    // 在事务中执行其他操作
    // 如果返回错误,整个事务会回滚
    return tx.Create(&UserLog{
        UserID: u.ID,
        Action: "created",
    }).Error
}

8.8 基于表的回调

注册回调

// 注册全局回调
db.Callback().Create().Before("gorm:create").Register("my_plugin:before_create", func(db *gorm.DB) {
    // 设置创建人
    if db.Statement.Schema != nil {
        if field := db.Statement.Schema.LookUpField("CreatedBy"); field != nil {
            // 从 context 获取当前用户
            if userID := db.Statement.Context.Value("userID"); userID != nil {
                field.Set(db.Statement.ReflectValue, userID)
            }
        }
    }
})

回调执行顺序

// 创建操作的回调顺序:
// before_create -> create -> after_create

// 查看所有回调
for name, processors := range db.Callback().Create().Processors {
    fmt.Println(name)
    for _, p := range processors {
        fmt.Println("  -", p.Name)
    }
}

8.9 移除回调

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

// 替换回调
db.Callback().Create().Replace("gorm:create", func(db *gorm.DB) {
    // 自定义创建逻辑
})

8.10 实战:审计日志

// AuditLog 审计日志模型
type AuditLog struct {
    ID          uint      `gorm:"primaryKey"`
    TableName   string
    RecordID    uint
    Operation   string    // CREATE/UPDATE/DELETE
    OldData     string    // JSON
    NewData     string    // JSON
    ChangedBy   uint      // 操作用户ID
    ChangedAt   time.Time
}

// Auditable 可审计接口
type Auditable interface {
    GetID() uint
    TableName() string
}

// 注册审计回调
func RegisterAuditCallbacks(db *gorm.DB) {
    // 创建审计
    db.Callback().Create().After("gorm:create").Register("audit:create", func(db *gorm.DB) {
        audit(db, "CREATE", nil, db.Statement.ReflectValue.Interface())
    })
    
    // 更新审计
    db.Callback().Update().Before("gorm:update").Register("audit:update:before", func(db *gorm.DB) {
        // 查询旧数据
        if db.Statement.Schema != nil {
            db.Statement.Set("audit:old_data", getOldData(db))
        }
    })
    
    db.Callback().Update().After("gorm:update").Register("audit:update:after", func(db *gorm.DB) {
        oldData, _ := db.Statement.Get("audit:old_data")
        audit(db, "UPDATE", oldData, db.Statement.ReflectValue.Interface())
    })
    
    // 删除审计
    db.Callback().Delete().Before("gorm:delete").Register("audit:delete:before", func(db *gorm.DB) {
        db.Statement.Set("audit:old_data", getOldData(db))
    })
    
    db.Callback().Delete().After("gorm:delete").Register("audit:delete:after", func(db *gorm.DB) {
        oldData, _ := db.Statement.Get("audit:old_data")
        audit(db, "DELETE", oldData, nil)
    })
}

func audit(db *gorm.DB, operation string, oldData, newData interface{}) {
    userID := db.Statement.Context.Value("userID")
    if userID == nil {
        userID = uint(0)
    }
    
    oldJSON, _ := json.Marshal(oldData)
    newJSON, _ := json.Marshal(newData)
    
    log := AuditLog{
        TableName: db.Statement.Table,
        Operation: operation,
        OldData:   string(oldJSON),
        NewData:   string(newJSON),
        ChangedBy: userID.(uint),
        ChangedAt: time.Now(),
    }
    
    // 使用新会话避免触发钩子循环
    db.Session(&gorm.Session{SkipHooks: true}).Create(&log)
}

func getOldData(db *gorm.DB) interface{} {
    // 实现查询旧数据逻辑
    return nil
}

8.11 实战:软删除扩展

// SoftDeleteModel 扩展软删除
type SoftDeleteModel struct {
    ID        uint           `gorm:"primaryKey"`
    DeletedAt gorm.DeletedAt `gorm:"index"`
    DeletedBy uint           // 删除人
}

func (m *SoftDeleteModel) BeforeDelete(tx *gorm.DB) (err error) {
    // 记录删除人
    if userID := tx.Statement.Context.Value("userID"); userID != nil {
        tx.Statement.SetColumn("DeletedBy", userID)
    }
    return
}

// Restore 恢复软删除
func (m *SoftDeleteModel) Restore(db *gorm.DB) error {
    return db.Model(m).Unscoped().Update("deleted_at", nil).Error
}

8.12 练习题

  1. 为订单模型添加钩子:创建时自动生成订单号(格式:年月日+6位流水号)
  2. 实现一个乐观锁钩子,在更新时检查版本号
  3. 编写一个自动填充多语言字段的钩子(根据当前语言环境)

8.13 小结

本章详细讲解了 GORM 的钩子与回调机制,包括各种生命周期钩子的使用和自定义回调注册。合理使用钩子可以实现审计日志、数据加密、自动填充等通用功能,但要避免在钩子中执行耗时操作。


本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo