Skip to content
鼓励作者:欢迎打赏犒劳

gorm钩子


✅ 一、表结构(已优化)

go
type User struct {
	ID        uint      `gorm:"primaryKey;column:id"`
	Name      string    `gorm:"column:name"`
	Email     string    `gorm:"column:email"`
	Age       uint8     `gorm:"column:age"`
	CreatedAt time.Time `gorm:"column:created_at"` // 推荐使用 time.Time
	UpdatedAt time.Time `gorm:"column:updated_at"`
}

✅ 二、常用 GORM 钩子说明

钩子方法触发时机典型用途
BeforeCreate创建前(INSERT 前)自动生成 ID、加密、设置默认值
AfterCreate创建成功后发送欢迎邮件、记录日志
BeforeUpdate更新前(UPDATE 前)数据校验、更新时间戳
AfterUpdate更新成功后触发事件、清理缓存
BeforeSave创建或更新前通用前置处理(如加密)
AfterSave创建或更新后通用后置操作
BeforeDelete删除前(硬删或软删前)权限校验、阻止删除
AfterDelete删除后清理关联数据
AfterFind查询后(Find/First/Scan 等)数据解密、字段转换

✅ 三、完整钩子示例代码

go
package main

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"time"
)

type User struct {
	ID        uint      `gorm:"primaryKey;column:id"`
	Name      string    `gorm:"column:name"`
	Email     string    `gorm:"column:email"`
	Age       uint8     `gorm:"column:age"`
	CreatedAt time.Time `gorm:"column:created_at"`
	UpdatedAt time.Time `gorm:"column:updated_at"`
}

// ============= 钩子实现 =============

// BeforeCreate 在创建前自动设置时间(CreatedAt 和 UpdatedAt)
func (u *User) BeforeCreate(tx *gorm.DB) error {
	now := time.Now()
	u.CreatedAt = now
	u.UpdatedAt = now
	fmt.Printf("[Hook] BeforeCreate: 设置创建时间 %v\n", now)
	return nil
}

// BeforeUpdate 在更新前更新 UpdatedAt
func (u *User) BeforeUpdate(tx *gorm.DB) error {
	u.UpdatedAt = time.Now()
	fmt.Printf("[Hook] BeforeUpdate: 更新时间戳为 %v\n", u.UpdatedAt)
	return nil
}

// BeforeSave 在保存前统一处理(创建和更新都触发)
// 注意:BeforeCreate 和 BeforeUpdate 会覆盖 BeforeSave 的部分行为
func (u *User) BeforeSave(tx *gorm.DB) error {
	// 通用校验
	if u.Name == "" {
		return fmt.Errorf("用户名不能为空")
	}
	if u.Age < 1 || u.Age > 150 {
		return fmt.Errorf("年龄必须在 1-150 之间")
	}
	fmt.Printf("[Hook] BeforeSave: 校验用户 %s, 年龄 %d\n", u.Name, u.Age)
	return nil
}

// AfterCreate 创建成功后触发
func (u *User) AfterCreate(tx *gorm.DB) error {
	fmt.Printf("[Hook] AfterCreate: 用户 %s (ID: %d) 已成功创建\n", u.Name, u.ID)
	// 可用于:发送欢迎邮件、初始化用户配置、记录审计日志等
	return nil
}

// AfterFind 查询后自动处理数据(如解密敏感字段)
func (u *User) AfterFind(tx *gorm.DB) error {
	// 示例:模拟对 Email 进行解密或脱敏
	// u.Email = decrypt(u.Email)
	fmt.Printf("[Hook] AfterFind: 加载用户 %s (ID: %d)\n", u.Name, u.ID)
	return nil
}

// 示例:软删除前校验(需配合 gorm.DeletedAt 使用)
// type User struct {
//     ...
//     DeletedAt gorm.DeletedAt `gorm:"index"`
// }
//
// func (u *User) BeforeDelete(tx *gorm.DB) error {
//     if u.Age < 18 {
//         return fmt.Errorf("未成年人禁止删除")
//     }
//     return nil
// }

✅ 四、使用示例

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// User 模型对应users表
type User struct {
	ID        uint      `gorm:"primaryKey;column:id"`
	Name      string    `gorm:"column:name"`
	Email     string    `gorm:"column:email"`
	Age       uint8     `gorm:"column:age"`
	CreatedAt time.Time `gorm:"column:created_at"`
	UpdatedAt time.Time `gorm:"column:updated_at"`
}

// TableName 指定表名
func (*User) TableName() string {
	return "users"
}

// ============= 钩子实现 =============

// BeforeCreate 在创建前自动设置时间(CreatedAt 和 UpdatedAt)
func (u *User) BeforeCreate(tx *gorm.DB) error {
	now := time.Now()
	u.CreatedAt = now
	u.UpdatedAt = now
	fmt.Printf("[Hook] BeforeCreate: 设置创建时间 %v\n", now)
	return nil
}

// BeforeUpdate 在更新前更新 UpdatedAt
func (u *User) BeforeUpdate(tx *gorm.DB) error {
	u.UpdatedAt = time.Now()
	fmt.Printf("[Hook] BeforeUpdate: 更新时间戳为 %v\n", u.UpdatedAt)
	return nil
}

// BeforeSave 在保存前统一处理(创建和更新都触发)
// 注意:BeforeCreate 和 BeforeUpdate 会覆盖 BeforeSave 的部分行为
func (u *User) BeforeSave(tx *gorm.DB) error {
	// 通用校验
	if u.Name == "" {
		return fmt.Errorf("用户名不能为空")
	}
	if u.Age < 1 || u.Age > 150 {
		return fmt.Errorf("年龄必须在 1-150 之间")
	}
	fmt.Printf("[Hook] BeforeSave: 校验用户 %s, 年龄 %d\n", u.Name, u.Age)
	return nil
}

// AfterCreate 创建成功后触发
func (u *User) AfterCreate(tx *gorm.DB) error {
	fmt.Printf("[Hook] AfterCreate: 用户 %s (ID: %d) 已成功创建\n", u.Name, u.ID)
	// 可用于:发送欢迎邮件、初始化用户配置、记录审计日志等
	return nil
}

// AfterFind 查询后自动处理数据(如解密敏感字段)
func (u *User) AfterFind(tx *gorm.DB) error {
	// 示例:模拟对 Email 进行解密或脱敏
	// u.Email = decrypt(u.Email)
	fmt.Printf("[Hook] AfterFind: 加载用户 %s (ID: %d)\n", u.Name, u.ID)
	return nil
}

// 使用示例
func main() {
	dsn := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"

	// 连接数据库
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // 设置日志级别为Info,显示所有SQL
	})
	if err != nil {
		log.Fatal("Failed to connect to database:", err)
	}
	// === 场景1:创建用户 ===
	user := User{
		Name:  "Bob",
		Email: "bob@example.com",
		Age:   30,
	}
	db.Create(&user)
	fmt.Printf("✅ 创建用户: %+v\n", user)

	// === 场景2:更新用户 ===
	user.Name = "Bob Chen"
	db.Save(&user) // 触发 BeforeUpdate, BeforeSave, AfterUpdate
	fmt.Printf("✅ 更新用户: %+v\n", user)

	// === 场景3:查询用户 ===
	var foundUser User
	db.First(&foundUser, user.ID) // 触发 AfterFind
	fmt.Printf("✅ 查询用户: %+v\n", foundUser)

	// === 场景4:批量更新(不触发单个模型的钩子)===
	db.Model(&User{}).Where("id = ?", user.ID).Update("age", 31)
	// ⚠️ 注意:批量更新不会触发 AfterUpdate、BeforeUpdate 等模型钩子!
	fmt.Println("⚠️ 批量更新完成(未触发模型钩子)")
}

✅ 五、关键注意事项 ⚠️

  1. 钩子签名必须正确

    go
    func (u *User) BeforeCreate(tx *gorm.DB) error
    • 必须是 指针接收者
    • 参数是 *gorm.DB
    • 返回 error
  2. 事务一致性

    • 钩子运行在同一个数据库事务中。
    • 如果钩子返回 error,整个操作将 回滚
  3. 避免在钩子中调用 Save/Create 导致死循环

    go
    func (u *User) AfterCreate(tx *gorm.DB) error {
        tx.Save(u) // ❌ 可能导致无限递归
        return nil
    }
  4. BeforeSave vs BeforeCreate/BeforeUpdate

    • BeforeSave创建和更新前 都会触发。
    • 如果同时定义了 BeforeCreateBeforeSave,执行顺序是:
      BeforeCreate → BeforeSave → SQL执行 → AfterCreate → AfterSave
  5. AfterFind 适用于数据后处理

    • 适合做:解密、脱敏、字段格式化。
    • 不会 触发数据库更新,即使你修改了字段。
  6. 批量操作不触发模型钩子!

    • db.Model(&User{}).Where(...).Update(...) ❌ 不触发 BeforeUpdate
    • db.Where(...).Delete(&User{}) ❌ 不触发 BeforeDelete
    • 只有单个模型的 Create, Save, Delete 等才会触发。
  7. 软删除钩子

    • 使用 gorm.DeletedAt 字段实现软删除。
    • BeforeDelete 仅在 硬删除 时触发。
    • 软删除通过 AfterDelete 处理。
  8. 性能考虑

    • 钩子中避免耗时操作(如网络请求、复杂计算),会影响数据库性能。
  9. 测试钩子逻辑

    • 建议为钩子编写单元测试,确保数据校验、时间设置等逻辑正确。

✅ 六、总结

钩子推荐用途
BeforeCreate设置默认值、生成 ID、加密
BeforeUpdate更新时间戳、数据校验
BeforeSave通用前置校验(创建/更新)
AfterCreate发送通知、初始化资源
AfterFind数据解密、脱敏、日志
BeforeDelete删除权限校验

最佳实践建议

  • 使用 time.Time 存储时间字段。
  • 将业务校验放在 BeforeSaveBeforeCreate
  • 敏感数据加密/解密用 BeforeSaveAfterFind
  • 避免在钩子中做远程调用。
  • 注意批量操作不触发钩子!

如有转载或 CV 的请标注本站原文地址