更新记录时,GORM 会依次触发 BeforeSave、BeforeUpdate、AfterUpdate、AfterSave 钩子。
更新记录前执行:
func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now()
return nil
}
更新记录后执行:
func (u *User) AfterUpdate(tx *gorm.DB) error {
return updateSearchIndex(u)
}
保存前执行,创建和更新都会触发:
func (u *User) BeforeSave(tx *gorm.DB) error {
u.Name = strings.TrimSpace(u.Name)
return nil
}
保存后执行,创建和更新都会触发:
func (u *User) AfterSave(tx *gorm.DB) error {
return clearUserCache(u.ID)
}
更新记录时的完整顺序:
BeforeSave
↓
BeforeUpdate
↓
执行 UPDATE
↓
AfterUpdate
↓
AfterSave
判断字段是否被修改:
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Password") {
hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), 10)
if err != nil {
return err
}
u.Password = string(hashed)
}
return nil
}
检查多个字段:
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Name", "Email") {
log.Printf("用户信息变更: ID=%d", u.ID)
}
return nil
}
获取更新前的值:
func (u *User) BeforeUpdate(tx *gorm.DB) error {
var oldUser User
if err := tx.First(&oldUser, u.ID).Error; err != nil {
return err
}
if oldUser.Status != u.Status {
log.Printf("状态变更: %s -> %s", oldUser.Status, u.Status)
}
return nil
}
注意:这会执行额外的查询。
只在特定条件下执行:
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.Status == "banned" {
var count int64
tx.Model(&User{}).Where("id = ? AND status != ?", u.ID, "banned").Count(&count)
if count > 0 {
notifyAdmin("用户被封禁", u.ID)
}
}
return nil
}
记录更新历史:
func (u *User) AfterUpdate(tx *gorm.DB) error {
history := UserHistory{
UserID: u.ID,
Action: "update",
Data: toJSON(u),
CreatedAt: time.Now(),
}
return tx.Create(&history).Error
}
返回错误阻止更新:
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.Role == "super_admin" {
return errors.New("超级管理员不能修改")
}
return nil
}
使用 UpdateColumn 跳过更新钩子:
db.Model(&user).UpdateColumn("name", "新名字")
或者:
db.Session(&gorm.Session{SkipHooks: true}).Model(&user).Update("name", "新名字")
Save 方法会触发钩子:
user.Name = "新名字"
db.Save(&user)
触发 BeforeSave -> BeforeUpdate -> AfterUpdate -> AfterSave。
Updates 方法也会触发钩子:
db.Model(&user).Updates(User{Name: "新名字"})
批量更新时,钩子不会触发:
db.Model(&User{}).Where("status = ?", "inactive").Update("status", "active")
这是为了性能考虑。如果需要触发钩子,需要逐条更新:
var users []User
db.Where("status = ?", "inactive").Find(&users)
for _, user := range users {
db.Model(&user).Update("status", "active")
}
密码更新
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Password") {
if len(u.Password) < 6 {
return errors.New("密码长度至少6位")
}
hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), 10)
if err != nil {
return err
}
u.Password = string(hashed)
}
return nil
}
状态变更通知
func (o *Order) AfterUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Status") {
switch o.Status {
case "paid":
go notifyMerchant(o)
case "shipped":
go notifyCustomer(o)
case "completed":
go updateStatistics(o)
}
}
return nil
}
库存检查
func (p *Product) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Stock") && p.Stock < 0 {
return errors.New("库存不能为负数")
}
return nil
}
更新钩子适合处理字段变更检测、数据验证、审计日志等逻辑。注意 UpdateColumn 和批量更新不会触发钩子。