vault backup: 2026-04-28 20:56:51
This commit is contained in:
+153
-37
@@ -11,23 +11,38 @@ create time: 2026-04-28 00:00
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Create 调用] --> BC["BeforeCreate"]
|
||||
BC --> CreateSQL["执行 INSERT"]
|
||||
CreateSQL --> AC["AfterCreate"]
|
||||
subgraph CREATE["🟢 Create"]
|
||||
A[Create 调用] --> BC["BeforeCreate"]
|
||||
BC --> CreateSQL["执行 INSERT"]
|
||||
CreateSQL --> AC["AfterCreate"]
|
||||
end
|
||||
|
||||
D[Update 调用] --> BU["BeforeUpdate"]
|
||||
BU --> UpdateSQL["执行 UPDATE"]
|
||||
UpdateSQL --> AU["AfterUpdate"]
|
||||
subgraph UPDATE["🟡 Update"]
|
||||
D[Update 调用] --> BU["BeforeUpdate"]
|
||||
BU --> UpdateSQL["执行 UPDATE"]
|
||||
UpdateSQL --> AU["AfterUpdate"]
|
||||
end
|
||||
|
||||
E[Delete 调用] --> BD["BeforeDelete"]
|
||||
BD --> DeleteSQL["执行 DELETE"]
|
||||
DeleteSQL --> AD["AfterDelete"]
|
||||
subgraph DELETE["🔴 Delete"]
|
||||
E[Delete 调用] --> BD["BeforeDelete"]
|
||||
BD --> DeleteSQL["执行 DELETE"]
|
||||
DeleteSQL --> AD["AfterDelete"]
|
||||
end
|
||||
|
||||
F[Find/First 调用] --> AF["AfterFind"]
|
||||
subgraph FIND["🔵 Find/Query"]
|
||||
F[Find/First 调用] --> AF["AfterFind"]
|
||||
end
|
||||
|
||||
style BC fill:#EAB308,color:#fff
|
||||
style BU fill:#F59E0B,color:#000
|
||||
style CREATE fill:#22C55E,color:#fff,stroke:#16A34A
|
||||
style UPDATE fill:#EAB308,color:#000,stroke:#CA8A04
|
||||
style DELETE fill:#EF4444,color:#fff,stroke:#DC2626
|
||||
style FIND fill:#3B82F6,color:#fff,stroke:#2563EB
|
||||
style BC fill:#22C55E,color:#fff
|
||||
style AC fill:#22C55E,color:#fff
|
||||
style BU fill:#EAB308,color:#000
|
||||
style AU fill:#EAB308,color:#000
|
||||
style BD fill:#EF4444,color:#fff
|
||||
style AD fill:#EF4444,color:#fff
|
||||
style AF fill:#3B82F6,color:#fff
|
||||
```
|
||||
|
||||
@@ -86,6 +101,11 @@ func (u *User) AfterUpdate(tx *gorm.DB) error {
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] Changed vs Updated
|
||||
> - `tx.Statement.Changed("field")`:只要调用过 `Updates(map)` 并包含该字段,就返回 true(无论新旧值是否相同)
|
||||
> - `tx.Statement.Updated("field")`:返回值与 Changed 一致,但额外校验「新值 ≠ 旧值」
|
||||
> - 如果直接通过结构体赋值再调 `Save()` / `UpdateColumn()`,这些方法都**不会追踪变化**,需用 `Updates` 才有效
|
||||
|
||||
### 删除阶段
|
||||
|
||||
```go
|
||||
@@ -151,16 +171,32 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
| `Deleted()` | 是否是 Delete 操作 |
|
||||
| `Updated(field)` | 字段是否被更新且值发生变化 |
|
||||
|
||||
## 全局钩子 vs Model 级钩子
|
||||
### Model 级 vs 全局 Callback
|
||||
|
||||
GORM 支持两种注册方式,优先级为 **Model 级 > 全局**:
|
||||
|
||||
```go
|
||||
// Model 级钩子——写在 struct 的方法上(最常用)
|
||||
// Model 级钩子——实现 gorm.LifecycleHooks 接口(最常用)
|
||||
type Order struct{}
|
||||
func (Order) BeforeCreate(tx *gorm.DB) error { ... }
|
||||
```
|
||||
|
||||
// 全局钩子——通过 Callback 注册,作用于所有模型
|
||||
> [!note] LifecycleHooks 接口
|
||||
> GORM v2 定义了以下接口,struct 只要方法签名匹配即自动注册为钩子:
|
||||
> - `BeforeCreate(tx *gorm.DB) error`
|
||||
> - `AfterCreate(tx *gorm.DB) error`
|
||||
> - `BeforeUpdate(tx *gorm.DB) error`
|
||||
> - `AfterUpdate(tx *gorm.DB) error`
|
||||
> - `BeforeDelete(tx *gorm.DB) error`
|
||||
> - `AfterDelete(tx *gorm.DB) error`
|
||||
> - `AfterFind(tx *gorm.DB) error`
|
||||
>
|
||||
> 你也可以使用 `*gorm.State` 替代 `*gorm.DB`(gint-gorm 兼容模式),但在大多数场景下 `*gorm.DB` 更通用。
|
||||
|
||||
#### 全局 Callback —— 作用于所有模型
|
||||
|
||||
```go
|
||||
// 在 Create SQL 执行之前注册自定义逻辑
|
||||
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 {
|
||||
@@ -169,14 +205,27 @@ db.Callback().Create().Before("gorm:create").Register("set_created_at", func(tx
|
||||
}
|
||||
})
|
||||
|
||||
// 使用场景:给所有模型统一设置默认时间(测试用)
|
||||
db.Session(&gorm.Session{Context: ctx}).Create(&order)
|
||||
// 在 Delete 之后追加逻辑(不阻塞后续 callback)
|
||||
db.Callback().Delete().After("gorm:delete").Register("cleanup_cache", func(tx *gorm.DB) {
|
||||
// 清理缓存等后置操作
|
||||
})
|
||||
|
||||
// 替换 GORM 内置回调(谨慎使用!)
|
||||
db.Callback().Create().Replace("gorm:create", func(tx *gorm.DB) {
|
||||
// 完全接管 Create 流程
|
||||
})
|
||||
|
||||
// 移除内置回调
|
||||
db.Callback().Create().Remove("gorm:create")
|
||||
```
|
||||
|
||||
> [!warning] 全局钩子注意事项
|
||||
> 全局钩子的注册时机必须在 `db.Open()` 之后、首次操作之前。而且全局钩子会影响**所有模型**——包括内置的 `gorm.Model`——使用时需格外小心。
|
||||
> 1. 注册时机必须在 `db.Open()` 之后、首次操作之前
|
||||
> 2. 会影响**所有模型**——包括内置的 `gorm.Model`
|
||||
> 3. 按阶段排序:`Before(name)` / `After(name)` / `Register(name, fn)` / `Replace(name, fn)` / `Remove(name)`
|
||||
> 4. 频繁读写数据库的全局逻辑会拖慢所有模型,建议在钩子内用 `tx.Get()` 做条件过滤
|
||||
|
||||
## 钩子执行链示意图
|
||||
## 执行时序
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -190,32 +239,98 @@ sequenceDiagram
|
||||
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 脱敏污染了原始数据 | 直接修改结构体字段影响调用方 | 需要脱敏时在接口层处理,不要改原对象 |
|
||||
下面展示一个电商订单模型,综合运用多个钩子处理真实场景:
|
||||
|
||||
## 钩子应用场景总结
|
||||
```go
|
||||
type Order struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"not null;index"`
|
||||
Amount decimal.Decimal
|
||||
Status string // pending → paid → shipped → completed
|
||||
PaidAt *time.Time // 支付时间,nil = 未支付
|
||||
Version int // 乐观锁版本号
|
||||
CreatedBy string // 创建人标识
|
||||
}
|
||||
|
||||
| 场景 | 推荐钩子 | 说明 |
|
||||
func (o *Order) BeforeCreate(tx *gorm.DB) error {
|
||||
// ① 初始化状态和审计字段
|
||||
if o.Status == "" {
|
||||
o.Status = "pending"
|
||||
}
|
||||
o.CreatedBy = getCurrentUserID() // 从 context 获取
|
||||
o.Version = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Order) BeforeUpdate(tx *gorm.DB) error {
|
||||
// ② 版本号递增 + 状态机校验
|
||||
o.Version++
|
||||
|
||||
validTransitions := map[string][]string{
|
||||
"pending": {"paid"},
|
||||
"paid": {"shipped"},
|
||||
"shipped": {"completed"},
|
||||
}
|
||||
newStatus := tx.Statement.Schema.ValueOfField(tx.Statement.Schema.FieldsByName["Status"])
|
||||
allowed, ok := validTransitions[o.Status]
|
||||
if !ok || !contains(allowed, newStatus.String()) {
|
||||
return fmt.Errorf("非法状态转换: %s → %s", o.Status, newStatus)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Order) AfterUpdate(tx *gorm.DB) error {
|
||||
// ③ 状态变更时发送通知
|
||||
if old, ok := tx.Data.(*Order); ok && old.Status != o.Status {
|
||||
sendNotification(o.UserID, "订单状态变更为: "+o.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Order) AfterDelete(tx *gorm.DB) error {
|
||||
// ④ 软删除后清理缓存
|
||||
redis.Del(context.Background(), "order:"+strconv.Itoa(int(o.ID)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(slice []string, s string) bool {
|
||||
for _, v := range slice {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] 为什么 AfterDelete 能拿到被删的数据?
|
||||
> 因为 GORM 的软删除是先查再标记 `deleted_at`,AfterDelete 钩子中仍然可以通过 `tx.Statement.ReflectValue` 访问到原始对象数据。
|
||||
|
||||
## 常用场景速查
|
||||
|
||||
| 需求 | 推荐方案 | 说明 |
|
||||
|------|---------|------|
|
||||
| 自动填充 CreatedAt / UpdatedAt | `autoTime` tag 更简单 | 如果手动实现用 BeforeCreate / BeforeUpdate |
|
||||
| 密码哈希 | BeforeCreate + BeforeUpdate(仅当 Changed) | 避免重复哈希 |
|
||||
| 自动填充 CreatedAt / UpdatedAt | 用 `autoTime` tag 更简单 | 手动实现则放 BeforeCreate / BeforeUpdate |
|
||||
| 密码哈希 | BeforeCreate + BeforeUpdate(仅 Changed) | 避免重复哈希 |
|
||||
| 乐观锁 | BeforeUpdate(检查 version) | 并发安全的经典方案 |
|
||||
| 审计日志 | AfterCreate / AfterUpdate / AfterDelete | 操作完成后异步记录 |
|
||||
| 数据脱敏 | AfterFind | 对外输出前格式化 |
|
||||
| 审计日志 | AfterCreate / AfterUpdate / AfterDelete | 操作完成后记录 |
|
||||
| 数据脱敏 | AfterFind | 对外输出前格式化(注意性能) |
|
||||
| 关联清理 | BeforeDelete | 删除前解除外部引用 |
|
||||
| 全局默认值 | Callback.Register() | 作用于所有模型,需条件过滤 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
> [!checklist] 使用钩子时的 Checklist
|
||||
> 1. **永远用 tx 不用 db** — 钩子内部不要调用 `db.Create()` / `db.Raw()`,必须使用传入的 `tx` 参数,否则会导致死锁
|
||||
> 2. **AfterFind 中不要修改原对象** — 直接改字段会影响所有调用方;需要脱敏时在接口层另行处理
|
||||
> 3. **不要在钩子里发送 HTTP 请求或做耗时 IO** — 会阻塞主流程;改用 channel + goroutine 异步处理
|
||||
> 4. **Changed/Updated 只在 Updates 时有效** — Save 是全量写入、Select/Omit 不会触发追踪
|
||||
> 5. **批量操作也会逐条触发** — `Create(&[]Model{})` 每条都走钩子;如想跳过用 `db.Session(&gorm.Session{SkipHooks: true}).Create(...)`
|
||||
> 6. **钩子返回 nil != 没返回值** — Go 中空指针 nil 不等于 error 零值,中断操作必须 `return errors.New("原因")`
|
||||
|
||||
## 关联笔记
|
||||
|
||||
@@ -223,3 +338,4 @@ sequenceDiagram
|
||||
- [[03-CRUD 操作]]
|
||||
- [[08-事务管理]]
|
||||
- [[11-批量操作]]
|
||||
- [[10-字段标签]](autoTime / autoCreateTime 等内置标签)
|
||||
|
||||
Reference in New Issue
Block a user