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),结果会多加一次。解法是改用 Create 或 Update 明确指定操作类型,别用语义模糊的 Save。
钩子里怎么拿到当前事务?
钩子函数签名 func (u *User) BeforeCreate(tx *gorm.DB) error 中的 tx 就是当前事务。用 tx 而不是全局 db,这样钩子中的操作和主操作在同一个事务里,任何一步失败都会回滚。典型场景——AfterCreate 中创建关联记录:
gofunc (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 之前插入自定义逻辑:
godb.Callback().Create().Before("gorm:create").Register("my:before_create", func(db *gorm.DB) { // 自定义逻辑 })
注册名必须唯一,后注册的同名回调会覆盖前者。
写段代码
密码加密 + 软删除保护的实际用法:
gofunc (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 }