GORM本身不处理自增主键冲突,而是原样抛出数据库的Duplicate entry错误;根本原因是默认使用标准INSERT语句,无冲突忽略逻辑,需从数据库自增机制(如MySQL重置AUTO_INCREMENT、PostgreSQL同步序列)和GORM使用方式(避免手动赋ID、优先用UUID、前置校验)协同解决。

GORM 本身不处理自增主键冲突,它只是把数据库的 Duplicate entry 错误原样抛出来。真正要解决,得从 MySQL 或 PostgreSQL 的自增机制入手,再配合 GORM 的使用方式调整。
为什么 GORM 批量插入会爆 Duplicate entry
根本不是 GORM “没做去重”,而是它默认走标准 INSERT,不带任何冲突忽略逻辑。常见触发点:
- 手动指定
ID值(比如迁移旧数据时写了id: 1001),而该值已在表中存在 - 恢复备份后没重置
AUTO_INCREMENT,比如表里最大id是5000,但SHOW TABLE STATUS显示Auto_increment = 100,下次插入就撞上 - 并发写入时两个 goroutine 都查了
MAX(id),都算出下一个该插5001,然后同时执行 —— TOCTOU 竞态,GORM 拦不住 - PostgreSQL 场景下手动插过数据,但没调
SELECT setval('seq_name', (SELECT MAX(id) FROM table)),序列还停在老位置
MySQL 表恢复后立即报错:怎么安全重置 AUTO_INCREMENT
别信 mysqldump 自动生成的 ALTER TABLE ... AUTO_INCREMENT=...,它只对空表有效。非空表必须手动校准:
- 先查真实最大值:
SELECT MAX(id) FROM users(注意结果为NULL时按 0 处理) - 再查当前自增值:
SHOW TABLE STATUS LIKE 'users',看Auto_increment字段 - 如果
MAX(id) >= Auto_increment,必须执行:ALTER TABLE users AUTO_INCREMENT = <max_id> + 1 - 执行前确保无写入,否则可能被并发 INSERT 干扰;执行后立刻用
SHOW CREATE TABLE users验证是否生效
GORM 中避免显式指定 ID 导致的冲突
除非是历史数据迁移等强需求,否则永远不要在结构体里手动赋值 ID 字段。GORM 会自动跳过零值 ID 并交由数据库生成:
- 结构体定义用
gorm.Model或显式声明ID uint,别写ID int(避免负数或零值干扰) - 插入前别给
ID赋值,哪怕写成user.ID = 0也比user.ID = 1001安全 - 如果业务真需要固定 ID(如导出导入映射),改用 UUID 字段(
string类型 +type: uuidtag),让主键和自增解耦 - 批量导入时,先用
SELECT id FROM users WHERE id IN (?...)检查冲突 ID 列表,再过滤掉已存在项 —— 比靠 GORM 报错后重试更可控
PostgreSQL 序列未同步怎么办
PostgreSQL 不像 MySQL 那样有表级 AUTO_INCREMENT,它依赖独立序列对象(如 users_id_seq)。手动插数据后,序列不会自动前进:
- 查当前序列值:
SELECT currval('users_id_seq')(仅对当前 session 有效)或SELECT last_value FROM users_id_seq - 查表中最大 ID:
SELECT MAX(id) FROM users - 若
last_value <= MAX(id),执行:SELECT setval('users_id_seq', (SELECT MAX(id) FROM users)) - 注意:
setval第二个参数是“设到哪”,不是“加多少”;设完后下一次NEXTVAL才会从该值+1开始
最易被忽略的一点:GORM 的 CreateInBatches 或事务内多条 Create,只要其中一条触发主键冲突,整个事务就回滚。你没法只跳过那一条 —— 想做到粒度控制,必须提前查、提前过滤,或者换用数据库原生的 upsert 语法(如 MySQL 的 INSERT IGNORE),再通过 db.Exec 手动执行。


















