9.8 KiB
tags, create time
| tags | create time | |||||||
|---|---|---|---|---|---|---|---|---|
|
2026-04-28 00:00 |
批量操作
概述
当数据量达到千级以上时,逐条操作的性能问题变得不可忽视。批量操作用于在一次数据库往返中处理大量记录——无论是插入、更新还是删除,都能显著降低网络开销和事务粒度。
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 — 批量插入
基本用法
// 直接传入 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 过大导致内存溢出或数据库拒绝执行。你可以通过配置调整这个行为:db.Session(&gorm.Session{FullSaveRecords: true}).Create(&hugeSlice)
高速批量插入(Exec)
对于超大批量(数万行),GORM 提供了更高效的执行方式——跳过模型解析,直接执行原始 SQL:
// 方案一:使用 raw sql 手动拼接(推荐用于极大数据量)
values := make([]string, len(users))
args := make([]any, 0, len(users)*3)
for i, u := range users {
values[i] = fmt.Sprintf("(%d, %d, %s)",
u.CreatedAt.Unix(), u.Age, tx.Migrator().ColumnName(u, "name"))
args = append(args, u.Name)
}
db.Exec(fmt.Sprintf("INSERT INTO users (created_at, age, name) VALUES %s",
strings.Join(values, ", "), args...))
// 方案二:用 CreateInBatches(内置分批,更简单)
err := db.CreateInBatches(&users, 500).Error // 每批 500 条
[!important] CreateInBatches vs Create
特性 Create CreateInBatches 参数 总数量 每批大小(batch size) 钩子触发 每条都触发 每条都触发 适用场景 中小批量(≤10K) 超大批量(>10K) 可控性 GORM 内部决定批次 可自定义 batch size
// 演示差异
db.CreateInBatches(&users, 100) // 每批 100 条
// users 有 350 条 → 分成 4 批:100, 100, 100, 50
db.CreateInBatches(&users, 200) // 每批 200 条
// users 有 350 条 → 分成 2 批:200, 150
[!question] 思考题 为什么 CreateInBatches 的第一个参数是 slice,第二个是 batch size?为什么不叫
batchSize而用total?答案:因为设计时参考的是「总共要处理的总数」语义。但在实际使用中,把它理解为「每批大小」更加直观。注意它不是总上限,而是批次阈值。
Update — 批量更新
map 方式(简洁但功能受限)
// 将所有状态为 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 控制字段
// 只更新指定字段(不管 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 方式
// 根据条件批量更新——注意只有非零值会被写入
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 — 批量删除
// 批量删除满足条件的记录
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 确认影响范围:// 先检查再删除 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 语法
// 用户注册时使用:已存在则更新最后登录时间,不存在则新建
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 语法
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 |
// 冲突时递增计数器而非覆盖
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{"count"}),
}).Create(&KV{})
// 如果 key 已存在 → count = count + 1 (由 GORM 自动处理)
Callback 机制——自定义批量行为
GORM 允许你在特定操作阶段注入自己的逻辑,实现真正的批量定制:
// 在批量创建完成后自动发送通知
db.Callback().Create().After("gorm:create").Register("send_notifications", func(tx *gorm.DB) {
// tx.Statement.Dest 包含被创建的数据
if dest, ok := tx.Statement.Dest.([]User); ok {
for _, user := range dest {
sendWelcomeEmail(user.Email)
}
}
})
[!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你可以在任意阶段的前后注册回调。
批量操作决策图
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 请求 | 合并为一次性批量调用或使用异步队列 |