--- tags: [GORM, Go, ORM, 批量操作, Bulk Insert, Batch Update, Callback] create time: 2026-04-28 00:00 --- # 批量操作 ## 概述 当数据量达到千级以上时,逐条操作的性能问题变得不可忽视。批量操作用于在一次数据库往返中处理大量记录——无论是插入、更新还是删除,都能显著降低网络开销和事务粒度。 ```mermaid flowchart LR Slow["N 条独立 SQL
N 次网络往返"] -->|优化后| Fast["1 条批量 SQL
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 确认范围
再 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-性能优化]]