Files
cs-note/hhs/GORM/11-批量操作.md
T
2026-05-24 11:42:38 +08:00

9.1 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
批量操作
Bulk Insert
Batch Update
Callback
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)

高速批量插入(CreateInBatches)

当数据量超过 10K 时,建议主动控制批次大小,避免单次 SQL 过大:

err := db.CreateInBatches(&users, 500).Error // 每批 500 条

[!tip] CreateInBatches vs Create

特性 Create CreateInBatches
内部实现 GORM 自动拆批(默认约 256 条) 按指定 batch size 拆分
适用场景 中小批量(≤10K) 超大批量(>10K),可控分批
参数控制 不可调 可自定义每批大小
// 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 方式(简洁但功能受限)

// 将所有状态为 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 允许你在特定操作阶段注入自己的逻辑,实现真正的批量定制:

// 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

你可以在任意阶段的前后注册回调。对于超大批量,建议在事务层控制而非逐条回调,避免性能瓶颈。

批量操作决策图

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 请求 合并为一次性批量调用或使用异步队列

关联笔记