钩子与回调
第八章:钩子与回调
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 练习题
- 为订单模型添加钩子:创建时自动生成订单号(格式:年月日+6位流水号)
- 实现一个乐观锁钩子,在更新时检查版本号
- 编写一个自动填充多语言字段的钩子(根据当前语言环境)
8.13 小结
本章详细讲解了 GORM 的钩子与回调机制,包括各种生命周期钩子的使用和自定义回调注册。合理使用钩子可以实现审计日志、数据加密、自动填充等通用功能,但要避免在钩子中执行耗时操作。
本文代码地址:https://github.com/LittleMoreInteresting/gorm_study
欢迎关注公众号,一起学习进步!