5月28日 02:42

GORM 钩子(Hooks)是怎么执行的?有哪些常见陷阱?

GORM 的钩子本质上是一组回调接口——只要你的 Model 实现了 BeforeCreate(tx *gorm.DB) error 这样的方法,GORM 就会在对应操作前后自动调用它。底层实现基于 GORM 的 callback 机制:每种操作(Create/Update/Delete/Query)维护一个有序的回调链,钩子函数被注册在链的特定位置,执行时按序逐个调用,任何一个返回 error 就中断并回滚事务。

关键执行顺序:

Create:BeforeSave → BeforeCreate → INSERT → AfterCreate → AfterSave

Update:BeforeSave → BeforeUpdate → UPDATE → AfterUpdate → AfterSave

Delete:BeforeDelete → DELETE → AfterDelete

Query:AfterFind(查几条触发几次)

注意 BeforeSave/AfterSave 是 Create 和 Update 共享的,这也是踩坑高发区。

追问

BeforeSave 里调用 tx.Save(u) 会怎样?

无限循环。BeforeSave → Save → 又触发 BeforeSave → 又 Save ……解决方案是用 tx.Session(&gorm.Session{SkipHooks: true}) 跳过钩子后再操作。同理,AfterCreate 里调 tx.Create 也会循环。

Save 方法为什么会触发两次 BeforeSave?

这是 GORM 的已知行为(issue #3971)。当主键非空但数据库中无此记录时,Save 内部先尝试 Create 再 Update,BeforeSave 被调用两次。如果你在 BeforeSave 里做累加操作(u.Age += 1),结果会多加一次。解法是改用 CreateUpdate 明确指定操作类型,别用语义模糊的 Save

钩子里怎么拿到当前事务?

钩子函数签名 func (u *User) BeforeCreate(tx *gorm.DB) error 中的 tx 就是当前事务。用 tx 而不是全局 db,这样钩子中的操作和主操作在同一个事务里,任何一步失败都会回滚。典型场景——AfterCreate 中创建关联记录:

go
func (u *User) AfterCreate(tx *gorm.DB) error { return tx.Create(&Profile{UserID: u.ID}).Error }

批量操作时钩子表现如何?

Create 传入切片时,钩子对每条记录逐一执行,数据量大时性能开销显著。用 db.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(...) 跳过钩子批量插入。另外,Create from map 方式(db.Model(&User{}).Create(map[string]interface{}{...}))本身就不触发钩子,因为 map 没有方法可调用。

怎么自定义回调顺序或替换默认钩子?

GORM 的 callback 链支持 Before("gorm:create")After("gorm:create")Replace("gorm:before_create", fn) 等操作。比如在 Create 之前插入自定义逻辑:

go
db.Callback().Create().Before("gorm:create").Register("my:before_create", func(db *gorm.DB) { // 自定义逻辑 })

注册名必须唯一,后注册的同名回调会覆盖前者。

写段代码

密码加密 + 软删除保护的实际用法:

go
func (u *User) BeforeSave(tx *gorm.DB) error { if u.Password != "" { u.Password = bcrypt.Hash(u.Password) } return nil } func (u *User) BeforeDelete(tx *gorm.DB) error { if !tx.Statement.Unscoped { return errors.New("请使用软删除") } return nil }
标签:Gorm