
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("⚠️ 批量更新完成(未触发模型钩子)")
}
✅ 五、关键注意事项 ⚠️
钩子签名必须正确
gofunc (u *User) BeforeCreate(tx *gorm.DB) error
- 必须是 指针接收者
- 参数是
*gorm.DB
- 返回
error
事务一致性
- 钩子运行在同一个数据库事务中。
- 如果钩子返回
error
,整个操作将 回滚。
避免在钩子中调用
Save
/Create
导致死循环gofunc (u *User) AfterCreate(tx *gorm.DB) error { tx.Save(u) // ❌ 可能导致无限递归 return nil }
BeforeSave
vsBeforeCreate
/BeforeUpdate
BeforeSave
在 创建和更新前 都会触发。- 如果同时定义了
BeforeCreate
和BeforeSave
,执行顺序是:BeforeCreate → BeforeSave → SQL执行 → AfterCreate → AfterSave
AfterFind
适用于数据后处理- 适合做:解密、脱敏、字段格式化。
- 不会 触发数据库更新,即使你修改了字段。
批量操作不触发模型钩子!
db.Model(&User{}).Where(...).Update(...)
❌ 不触发BeforeUpdate
db.Where(...).Delete(&User{})
❌ 不触发BeforeDelete
- 只有单个模型的
Create
,Save
,Delete
等才会触发。
软删除钩子
- 使用
gorm.DeletedAt
字段实现软删除。 BeforeDelete
仅在 硬删除 时触发。- 软删除通过
AfterDelete
处理。
- 使用
性能考虑
- 钩子中避免耗时操作(如网络请求、复杂计算),会影响数据库性能。
测试钩子逻辑
- 建议为钩子编写单元测试,确保数据校验、时间设置等逻辑正确。
✅ 六、总结
钩子 | 推荐用途 |
---|---|
BeforeCreate | 设置默认值、生成 ID、加密 |
BeforeUpdate | 更新时间戳、数据校验 |
BeforeSave | 通用前置校验(创建/更新) |
AfterCreate | 发送通知、初始化资源 |
AfterFind | 数据解密、脱敏、日志 |
BeforeDelete | 删除权限校验 |
✅ 最佳实践建议:
- 使用
time.Time
存储时间字段。 - 将业务校验放在
BeforeSave
或BeforeCreate
。 - 敏感数据加密/解密用
BeforeSave
和AfterFind
。 - 避免在钩子中做远程调用。
- 注意批量操作不触发钩子!