创建钩子

创建记录时,GORM 会依次触发 BeforeSave、BeforeCreate、AfterCreate、AfterSave 钩子。

BeforeCreate

创建记录前执行:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    u.UUID = uuid.New().String()
    return nil
}

AfterCreate

创建记录后执行:

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

BeforeSave

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

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

AfterSave

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

func (u *User) AfterSave(tx *gorm.DB) error {
    return updateCache(tx, u)
}

执行顺序

创建记录时的完整顺序:

BeforeSave
  ↓
BeforeCreate
  ↓
执行 INSERT
  ↓
AfterCreate
  ↓
AfterSave

钩子中的事务

钩子在同一个事务中执行:

func (u *User) AfterCreate(tx *gorm.DB) error {
    order := Order{
        UserID: u.ID,
        Status: "pending",
    }
    if err := tx.Create(&order).Error; err != nil {
        return err
    }
    
    if err := sendWelcomeEmail(u.Email); err != nil {
        return err
    }
    
    return nil
}

如果发送邮件失败,整个事务回滚,用户和订单都不会创建。

条件执行

根据条件决定是否执行:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Role == "" {
        u.Role = "user"
    }
    
    if u.Role == "admin" {
        var count int64
        tx.Model(&User{}).Where("role = ?", "admin").Count(&count)
        if count >= 10 {
            return errors.New("管理员数量已达上限")
        }
    }
    
    return nil
}

修改创建数据

在钩子中修改字段值:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    u.Name = strings.TrimSpace(u.Name)
    u.Email = strings.ToLower(u.Email)
    
    if u.Password != "" {
        hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), 10)
        if err != nil {
            return err
        }
        u.Password = string(hashed)
    }
    
    return nil
}

访问上下文

从钩子中获取请求上下文:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    ctx := tx.Statement.Context
    
    if userID, ok := ctx.Value("user_id").(uint); ok {
        u.CreatedBy = userID
    }
    
    return nil
}

// 使用
ctx := context.WithValue(context.Background(), "user_id", uint(1))
db.WithContext(ctx).Create(&user)

批量创建的钩子

批量创建时,每个记录都会触发钩子:

users := []User{
    {Name: "用户1"},
    {Name: "用户2"},
}
db.Create(&users)

BeforeCreate 会执行两次。

CreateInBatches 可能不同:

db.CreateInBatches(users, 100)

分批创建时,每批都会触发钩子。

跳过钩子

某些场景需要跳过钩子:

db.Session(&gorm.Session{SkipHooks: true}).Create(&user)

或者用原生 SQL:

db.Exec("INSERT INTO users (name) VALUES (?)", user.Name)

实际案例

用户注册

func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Name == "" {
        return errors.New("用户名不能为空")
    }
    
    if u.Email == "" {
        return errors.New("邮箱不能为空")
    }
    
    var count int64
    tx.Model(&User{}).Where("email = ?", u.Email).Count(&count)
    if count > 0 {
        return errors.New("邮箱已被注册")
    }
    
    if u.Password != "" {
        hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), 10)
        if err != nil {
            return err
        }
        u.Password = string(hashed)
    }
    
    u.Status = "active"
    u.CreatedAt = time.Now()
    
    return nil
}

func (u *User) AfterCreate(tx *gorm.DB) error {
    profile := UserProfile{
        UserID: u.ID,
    }
    if err := tx.Create(&profile).Error; err != nil {
        return err
    }
    
    go sendWelcomeEmail(u.Email)
    
    return nil
}

订单创建

func (o *Order) BeforeCreate(tx *gorm.DB) error {
    if o.UserID == 0 {
        return errors.New("用户ID不能为空")
    }
    
    if len(o.Items) == 0 {
        return errors.New("订单项不能为空")
    }
    
    o.OrderNo = generateOrderNo()
    o.Status = "pending"
    
    var total float64
    for _, item := range o.Items {
        total += item.Price * float64(item.Quantity)
    }
    o.TotalAmount = total
    
    return nil
}

func (o *Order) AfterCreate(tx *gorm.DB) error {
    for i := range o.Items {
        o.Items[i].OrderID = o.ID
    }
    return tx.Create(&o.Items).Error
}

小结

创建钩子适合处理数据验证、默认值设置、关联创建等逻辑。注意钩子在事务中执行,返回错误会回滚整个操作。