更新钩子

更新记录时,GORM 会依次触发 BeforeSave、BeforeUpdate、AfterUpdate、AfterSave 钩子。

BeforeUpdate

更新记录前执行:

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    u.UpdatedAt = time.Now()
    return nil
}

AfterUpdate

更新记录后执行:

func (u *User) AfterUpdate(tx *gorm.DB) error {
    return updateSearchIndex(u)
}

BeforeSave

保存前执行,创建和更新都会触发:

func (u *User) BeforeSave(tx *gorm.DB) error {
    u.Name = strings.TrimSpace(u.Name)
    return nil
}

AfterSave

保存后执行,创建和更新都会触发:

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

Save 方法会触发钩子:

user.Name = "新名字"
db.Save(&user)

触发 BeforeSave -> BeforeUpdate -> AfterUpdate -> AfterSave。

钩子与 Updates

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 和批量更新不会触发钩子。