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/11-批量操作.md
T
2026-04-28 20:56:51 +08:00

278 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [GORM, Go, ORM, 批量操作, Bulk Insert, Batch Update, Callback]
create time: 2026-04-28 00:00
---
# 批量操作
## 概述
当数据量达到千级以上时,逐条操作的性能问题变得不可忽视。批量操作用于在一次数据库往返中处理大量记录——无论是插入、更新还是删除,都能显著降低网络开销和事务粒度。
```mermaid
flowchart LR
Slow["N 条独立 SQL<br/>N 次网络往返"] -->|优化后| Fast["1 条批量 SQL<br/>1 次网络往返"]
style Slow fill:#EF4444,color:#fff
style Fast fill:#3B82F6,color:#fff
```
## Create — 批量插入
### 基本用法
```go
// 直接传入 slice —— GORM 自动生成多 VALUES INSERT
users := []User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
{Name: "Charlie", Age: 28},
}
result := db.Create(&users)
fmt.Println(result.RowsAffected) // 3
// 自动回填 ID
for _, u := range users {
fmt.Printf("ID=%d Name=%s\n", u.ID, u.Name)
}
```
> [!tip] 底层机制:拆批策略
> GORM 内部会将大 slice 拆成多个 `INSERT INTO ... VALUES (...), (...), ...` 语句执行,每批默认约 **256 条**。这样做是为了避免单条 SQL 过大导致内存溢出或数据库拒绝执行。你可以通过配置调整这个行为:
> ```go
> db.Session(&gorm.Session{FullSaveRecords: true}).Create(&hugeSlice)
> ```
### 高速批量插入(CreateInBatches)
当数据量超过 10K 时,建议主动控制批次大小,避免单次 SQL 过大:
```go
err := db.CreateInBatches(&users, 500).Error // 每批 500 条
```
> [!tip] CreateInBatches vs Create
> | 特性 | Create | CreateInBatches |
> |------|--------|-----------------|
> | 内部实现 | GORM 自动拆批(默认约 256 条) | 按指定 batch size 拆分 |
> | 适用场景 | 中小批量(≤10K) | 超大批量(>10K),可控分批 |
> | 参数控制 | 不可调 | 可自定义每批大小 |
```go
// 350 条数据 → 4 批:100, 100, 100, 50
db.CreateInBatches(&users, 100).Error
// 350 条数据 → 2 批:200, 150
db.CreateInBatches(&users, 200).Error
```
> [!question] 思考题
> 如果 users 只有 50 条记录,传入 batch size = 100,会发生什么?
>
> > **答案**:不会报错,只会执行一批——包含全部 50 条。`CreateInBatches` 的行为是「向上取整分批次」,没有足够数据也不会跳过执行。
## Update — 批量更新
### map 方式(简洁但功能受限)
```go
// 将所有状态为 trial 的用户升级为 active
result := db.Model(&User{}).
Where("status = ?", "trial").
Updates(map[string]any{
"status": "active",
"updated_at": time.Now(),
})
fmt.Printf("更新了 %d 条记录\n", result.RowsAffected)
// UPDATE users SET status='active', updated_at=... WHERE status='trial';
```
### Select / Omit 控制字段
```go
// 只更新指定字段(不管 map 里写了多少 key,只更新白名单中的字段)
db.Model(&User{}).
Where("status = ?", "trial").
Select("status", "plan"). // 白名单
Updates(map[string]any{
"status": "active",
"plan": "pro",
"email": "should_not_change@test.com", // 被忽略
})
// 排除某些字段
db.Model(&User{}).
Where("status = ?", "trial").
Omit("password", "secret_key"). // 黑名单
Updates(map[string]any{
"status": "active",
"plan": "pro",
"password": "new_password_here", // 被忽略
"updated_at": time.Now(),
})
```
> [!tip] Select + Omit 的区别
> - `Select` 是白名单——只更新列出的字段
> - `Omit` 是黑名单——除了列出的字段,其他都更新
> - 两者可以组合使用,最终生效 = Select(若有)∩ Not(Omit)
### Struct 方式
```go
// 根据条件批量更新——注意只有非零值会被写入
db.Model(&User{}).Where("score < ?", 60).Updates(User{Status: "reminded"})
// UPDATE users SET status='reminded' WHERE score < 60;
// ❌ 陷阱:如果 Status 的值就是零值 "",则不会被写入
db.Model(&User{}).Where("role = ?", "admin").Updates(User{Role: ""})
// 这条不会生成任何 SET 子句!
// ✅ 解决:用 map 替代 struct
db.Model(&User{}).Where("role = ?", "admin").Updates(map[string]any{"role": ""})
```
## Delete — 批量删除
```go
// 批量删除满足条件的记录
result := db.Where("age < ?", 18).Delete(&User{})
fmt.Printf("删除了 %d 条记录\n", result.RowsAffected)
// DELETE FROM users WHERE age < 18;
// 物理批量删除(跳过软删除过滤)
db.Unscoped().Where("deleted_at < ?", time.Now().AddDate(-1, 0, 0)).Delete(&User{})
```
> [!warning] 批量删除的威力
> `Where(...).Delete()` 是极其危险的操作——一条错误的 WHERE 可能瞬间清空百万行。**执行前务必先用 SELECT 确认影响范围**:
> ```go
> // 先检查再删除
> var count int64
> db.Model(&User{}).Where("status = ?", "inactive").Count(&count)
> if count == 0 {
> return nil
> }
> log.Printf("即将删除 %d 条 inactive 记录...", count)
> if !confirm() {
> return errors.New("用户取消操作")
> }
> db.Where("status = ?", "inactive").Delete(&User{})
> ```
## Upsert — 存在则更新,不存在则插入
GORM 通过 `Clauses(clause.OnConflict{})` 实现 MySQL 的 ON DUPLICATE KEY UPDATE 和 PostgreSQL 的 ON CONFLICT:
### MySQL 语法
```go
// 用户注册时使用:已存在则更新最后登录时间,不存在则新建
users := []User{
{Email: "alice@example.com", LastLogin: time.Now()},
{Email: "bob@example.com", LastLogin: time.Now()},
}
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"last_login"}),
}).Create(&users)
// MySQL: INSERT INTO users (email, last_login) VALUES (...)
// ON DUPLICATE KEY UPDATE last_login=VALUES(last_login);
```
### PostgreSQL 语法
```go
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"last_login"}),
}).Create(&users)
// PostgreSQL: INSERT INTO users (email, last_login) VALUES (...)
// ON CONFLICT (email) DO UPDATE SET last_login = excluded.last_login;
```
### 常见冲突策略
| 策略 | 作用 | SQL 等价 |
|------|------|---------|
| `DoNothing` | 冲突时不操作 | `ON CONFLICT DO NOTHING` |
| `DoUpdates` | 冲突时更新指定字段 | `ON CONFLICT DO UPDATE SET ...` |
| `DoUpdates: clause.Assignments(expr)` | 自定义表达式 | `SET col = expr` |
```go
// 冲突时递增计数器而非覆盖
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{"count"}),
}).Create(&KV{})
// 如果 key 已存在 → count = count + 1 (由 GORM 自动处理)
```
## Callback 机制——自定义批量行为
GORM 允许你在特定操作阶段注入自己的逻辑,实现真正的批量定制:
```go
// GORM v2:通过 Session 注册回调
db.Session(&gorm.Session{
DryRun: true, // 仅生成 SQL 不执行,用于调试
}, func(tx *gorm.DB) error {
return tx.Create(&users).Error
})
// 也可以直接在模型上定义钩子(见 [[09-钩子函数]])
```
> [!tip] Callback 阶段排序
> GORM 的操作流程可拆解为明确的阶段,每个阶段都有对应的 Hook 点:
> ```
> CREATE: BeforeQuery → BeforeCreate → Create → AfterCreate → AfterQuery
> UPDATE: BeforeQuery → BeforeUpdate → Update → AfterUpdate → AfterQuery
> DELETE: BeforeQuery → BeforeDelete → Delete → AfterDelete → AfterQuery
> FIND: BeforeQuery → Query → AfterFind → AfterQuery
> ```
> 你可以在任意阶段的前后注册回调。对于超大批量,建议在事务层控制而非逐条回调,避免性能瓶颈。
## 批量操作决策图
```mermaid
flowchart TD
Start[需要批量操作数据] --> OpType{"操作类型"}
OpType --> |插入| InsertQ{"数据量?"}
InsertQ --> |< 10K| SimpleInsert["Create\\(slice\\)"]
InsertQ --> |≥ 10K| BatchInsert["CreateInBatches\\(batchSize\\)"]
OpType --> |更新| UpdateQ{"是否需要精准字段控制?"}
UpdateQ --> |不需要| MapUpdate["Updates\\(map\\)"]
UpdateQ --> |需要| FieldControl["Select / Omit + Updates"]
OpType --> |删除| DelCheck["先 Count 确认范围<br/>再 Delete"]
OpType --> |存在则更新| Upsert["Clauses\\(OnConflict\\)"]
style Start fill:#4FC08D,color:#fff
style BatchInsert fill:#3B82F6,color:#fff
style MapUpdate fill:#F59E0B,color:#000
style Upsert fill:#8B5CF6,color:#fff
```
## 常见坑点速查
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 批量插入太慢 | 单条 SQL 过大被数据库拒绝 | 用 CreateInBatches 控制批次大小 |
| 批量 Updates struct 跳过了零值 | struct 模式下零值被视为「未修改」 | 改用 Updates(map) |
| 误删大量数据 | WHERE 条件过于宽泛 | 先 SELECT COUNT 确认,再加 limit 逐步删除 |
| OnConflict 没生效 | 目标列没有唯一约束 | 确保冲突列上有 UNIQUE 或 PRIMARY KEY |
| 批量操作中钩子阻塞 | 每条都发 HTTP 请求 | 合并为一次性批量调用或使用异步队列 |
## 关联笔记
- [[03-CRUD 操作]]
- [[08-事务管理]]
- [[09-钩子函数]]
- [[11-批量操作]]
- [[15-性能优化]]