This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/09-钩子函数.md
T

226 lines
7.1 KiB
Markdown
Raw Normal View History

2026-04-28 20:23:33 +08:00
---
tags: [GORM, Go, ORM, 钩子函数, Callback, BeforeCreate, AfterUpdate, Lifecycle]
create time: 2026-04-28 00:00
---
# 钩子函数
## 概述
钩子(Hook)是 GORM 生命周期回调机制,允许你在特定操作前后注入自定义逻辑。它们是「横切关注点」的理想载体——不需要在每个业务代码里重复写加密、日志、审计等通用逻辑。
```mermaid
flowchart TD
A[Create 调用] --> BC["BeforeCreate"]
BC --> CreateSQL["执行 INSERT"]
CreateSQL --> AC["AfterCreate"]
D[Update 调用] --> BU["BeforeUpdate"]
BU --> UpdateSQL["执行 UPDATE"]
UpdateSQL --> AU["AfterUpdate"]
E[Delete 调用] --> BD["BeforeDelete"]
BD --> DeleteSQL["执行 DELETE"]
DeleteSQL --> AD["AfterDelete"]
F[Find/First 调用] --> AF["AfterFind"]
style BC fill:#EAB308,color:#fff
style BU fill:#F59E0B,color:#000
style BD fill:#EF4444,color:#fff
style AF fill:#3B82F6,color:#fff
```
> [!note] GORM 的操作顺序
> 每个操作的执行流:`钩子(前置) → SQL 执行 → 钩子(后置)`。前置钩子返回错误可以**中断整个操作**。
## 完整钩子列表
### 创建阶段
```go
func (u *User) BeforeCreate(tx *gorm.DB) error {
// 在 INSERT 之前执行
u.CreatedAt = time.Now()
u.UpdatedAt = time.Now()
// 对密码做哈希处理
hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
u.Password = string(hashed)
// 生成唯一 ID
if u.UUID == "" {
u.UUID = generateUUID()
}
return nil // 返回错误可中断插入
}
func (u *User) AfterCreate(tx *gorm.DB) error {
// 在 INSERT 之后执行
// 例如:发送欢迎邮件、记录审计日志
log.Printf("用户 %s 创建成功,ID=%d", u.Name, u.ID)
return nil
}
```
### 更新阶段
```go
func (u *User) BeforeUpdate(tx *gorm.DB) error {
// 在 UPDATE 之前执行
u.UpdatedAt = time.Now()
// 检查乐观锁版本号
var prev User
tx.Select("version").First(&prev, u.ID)
if prev.Version != u.Version {
return errors.New("数据已被其他人修改,请刷新后重试")
}
return nil
}
func (u *User) AfterUpdate(tx *gorm.DB) error {
// 同步缓存
redis.Set(context.Background(), "user:"+strconv.Itoa(u.ID), u, time.Hour)
return nil
}
```
### 删除阶段
```go
func (u *User) BeforeDelete(tx *gorm.DB) error {
// 清理关联数据(比如清除 Redis Session)
sessionIDs, _ := getSessionIDsByUserID(u.ID)
for _, id := range sessionIDs {
redis.Del(context.Background(), "session:"+id)
}
return nil
}
func (u *User) AfterDelete(tx *gorm.DB) error {
// 如果用了软删除,这里可以做真正的物理清理(如文件、附件)
if tx.RowsAffected > 0 {
deleteUploadedFiles(u)
}
return nil
}
```
### 查询阶段
```go
func (u *User) AfterFind(tx *gorm.DB) error {
// 在 SELECT 之后执行——每次 Find/First 都会触发
// 适合用于敏感字段脱敏或格式化处理
// 示例:邮箱脱敏
if len(u.Email) > 4 {
masked := u.Email[:2] + "***" + u.Email[len(u.Email)-2:]
fmt.Println("已脱敏:", masked)
}
return nil
}
```
> [!question] AfterFind 的性能影响
> `AfterFind` 会在每次查询时对所有行都执行——如果一次查出 1000 条记录,钩子会运行 1000 次。不要在 `AfterFind` 中放重逻辑(如 RPC 调用)。
## 条件判断:根据操作类型选择行为
有时你希望同一个钩子在特定操作下才生效。可以通过 `tx.Statement` 判断当前操作类型:
```go
func (u *User) BeforeUpdate(tx *gorm.DB) error {
// 只在 Password 被修改时才重新哈希
if tx.Statement.Changed("Password") {
hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
u.Password = string(hashed)
}
return nil
}
```
> [!tip] Statement 常用方法
| 方法 | 作用 |
|------|------|
| `Changed(field)` | 该字段是否在 Updates 中被修改 |
| `Select()` | 获取显式 SELECT 的字段列表 |
| `Omit()` | 获取 OMIT 的字段列表 |
| `Deleted()` | 是否是 Delete 操作 |
| `Updated(field)` | 字段是否被更新且值发生变化 |
## 全局钩子 vs Model 级钩子
GORM 支持两种注册方式,优先级为 **Model 级 > 全局**:
```go
// Model 级钩子——写在 struct 的方法上(最常用)
type Order struct{}
func (Order) BeforeCreate(tx *gorm.DB) error { ... }
// 全局钩子——通过 Callback 注册,作用于所有模型
db.Callback().Create().Before("gorm:create").Register("set_created_at", func(tx *gorm.DB) {
if v, ok := tx.Get("created_at_overwrite"); ok {
if t, ok := v.(time.Time); ok {
tx.Statement.SetColumn("CreatedAt", t)
}
}
})
// 使用场景:给所有模型统一设置默认时间(测试用)
db.Session(&gorm.Session{Context: ctx}).Create(&order)
```
> [!warning] 全局钩子注意事项
> 全局钩子的注册时机必须在 `db.Open()` 之后、首次操作之前。而且全局钩子会影响**所有模型**——包括内置的 `gorm.Model`——使用时需格外小心。
## 钩子执行链示意图
```mermaid
sequenceDiagram
participant App as 应用代码
participant Hook as GORM 钩子系统
participant DB as 数据库
App->>Hook: db.Create(&user)
Hook->>Hook: BeforeCreate
Hook->>DB: INSERT INTO users...
DB-->>Hook: 返回影响行数
Hook->>Hook: AfterCreate
Hook-->>App: 返回结果
Note over Hook: Update: BeforeUpdate → SQL → AfterUpdate
Note over Hook: Delete: BeforeDelete → SQL → AfterDelete
Note over Hook: Find: BeforeQuery → SQL → AfterFind
```
## 常见坑点速查
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 钩子里调用了 db(而不是 tx) | 死锁——钩子内再开事务嵌套 | 钩子内部全部使用 `tx` 参数 |
| Changed() 在 Save 时总返回 false | Save 是全量写入,不追踪变化 | 用 `Updates(struct)` 配合 Changed() |
| 钩子返回了 nil 但想中断操作 | 零值 nil 不是错误 | `return errors.New("中断原因")` |
| 批量操作也会触发钩子 | `Create(&[]User{})` 每条都走钩子 | 如需跳过可用 `SkipHooks` session |
| AfterFind 脱敏污染了原始数据 | 直接修改结构体字段影响调用方 | 需要脱敏时在接口层处理,不要改原对象 |
## 钩子应用场景总结
| 场景 | 推荐钩子 | 说明 |
|------|---------|------|
| 自动填充 CreatedAt / UpdatedAt | `autoTime` tag 更简单 | 如果手动实现用 BeforeCreate / BeforeUpdate |
| 密码哈希 | BeforeCreate + BeforeUpdate(仅当 Changed) | 避免重复哈希 |
| 乐观锁 | BeforeUpdate(检查 version) | 并发安全的经典方案 |
| 审计日志 | AfterCreate / AfterUpdate / AfterDelete | 操作完成后异步记录 |
| 数据脱敏 | AfterFind | 对外输出前格式化 |
| 关联清理 | BeforeDelete | 删除前解除外部引用 |
## 关联笔记
- [[02-模型定义]]
- [[03-CRUD 操作]]
- [[08-事务管理]]
- [[11-批量操作]]