gorm.save()和createinbatches()无法实现标准upsert,因save()逐条判断、createinbatches()不处理冲突;postgresql需显式指定唯一约束列并用excluded引用新值,mysql需共享同一参数列表且注意now()等函数用法,批量时须分批并确保索引存在。

为什么不能直接用 GORM.Save() 或 GORM.CreateInBatches() 做合并更新
因为 GORM 原生不支持标准 SQL 的 INSERT ... ON CONFLICT DO UPDATE(PostgreSQL)或 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)语义,Save() 是逐条判断是否存在再决定 INSERT/UPDATE,CreateInBatches() 只做批量插入、不处理冲突。真要“存在则更新、不存在则插入”,必须绕过 GORM 的 ORM 层写自定义 SQL。
PostgreSQL 下用 ON CONFLICT 实现 upsert 的关键点
PostgreSQL 的 upsert 依赖唯一约束(UNIQUE 或 PRIMARY KEY),GORM 不会自动推导冲突字段,必须显式指定。常见错误是漏写 DO UPDATE SET 后的字段别名或误用 EXCLUDED。
- 确保目标表有唯一索引(比如在
user_id或(tenant_id, code)上) -
ON CONFLICT后必须跟明确定义的索引列或列组合,不能只写表名 -
DO UPDATE SET中引用新数据要用EXCLUDED.column_name,不是VALUES()或别名 - GORM
Session().Exec()才能安全传参,避免 SQL 注入;不要拼接字符串
db.Session(&session).Exec(`
INSERT INTO users (id, name, email, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
updated_at = EXCLUDED.updated_at
`, user.ID, user.Name, user.Email, time.Now())
MySQL 下用 ON DUPLICATE KEY UPDATE 的参数绑定陷阱
MySQL 的语法更宽松,但 GORM 的 Exec() 对 ? 占位符数量极其敏感:INSERT 和 UPDATE 部分的参数是同一组,不能重复传值,也不能少传。
- 所有
VALUES()里的?和UPDATE表达式里的?共享同一参数列表 - 如果 UPDATE 需要当前时间,不能写
NOW()再额外传参,应直接写函数(NOW()是安全的) - 避免在
UPDATE子句里用未出现在VALUES()中的变量,否则参数错位
db.Exec(`
INSERT INTO users (id, name, email, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
email = VALUES(email),
updated_at = NOW()
`, u.ID, u.Name, u.Email, time.Now(), time.Now())
批量 upsert 时如何避免参数超限和性能断崖
单条 SQL 插入 1000 行没问题,但若每行 5 个字段,就是 5000 个 ? —— 多数数据库有参数上限(如 MySQL 默认 65535),且 GORM 绑定大量参数会显著变慢。
- 按每批 100–300 行切分数据,用循环调用
Exec(),别试图一口吞下 10000 行 - 提前用
db.Migrator().CreateIndex()确保唯一索引存在,否则ON CONFLICT直接报错 - 如果业务允许,考虑先
DELETE WHERE id IN (?)再全量INSERT,比 upsert 更快(尤其无主键更新场景) - 注意事务包裹:upsert 本身是原子操作,但多批次需手动
db.Transaction()
最常被忽略的是索引缺失导致的静默失败——ON CONFLICT 没匹配到任何唯一约束,就退化成纯 INSERT,重复数据直接报错;而 MySQL 的 ON DUPLICATE KEY 在无唯一键时完全不触发更新。这点必须验证。











