gorm的onconflict必须显式指定冲突字段,不能依赖主键或唯一索引自动推断;需通过columns明确冲突条件,doupdates用assignmentcolumns白名单控制更新字段,避免误覆创建字段,且createinbatches不支持该功能。

用 OnConflict 指定冲突键,不是靠主键自动推断
GORM 的 OnConflict 不会默认按主键或唯一索引自动识别冲突条件。你必须显式传入 clause.OnConflict{Columns: []clause.Column{...}},否则即使有唯一约束,Upsert 也会退化为普通 INSERT 并报错(如 Duplicate entry)。
常见错误是只写 .OnConflict(clause.OnConflict{}) 空结构体,这在 MySQL 下实际无效。
正确做法是明确列出触发更新的字段,比如表中 f_name 是 UNIQUE KEY:
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "f_name"}},
DoUpdates: clause.AssignmentColumns([]string{"f_address", "f_update_uid", "f_update_time"}),
}).Create(&tableA)
注意:Columns 是“什么条件下算冲突”,DoUpdates 是“冲突时更新哪些字段”——两者必须分开指定。
更新字段要排除创建时间/创建人,用 AssignmentColumns 显式控制
业务要求不能覆盖 f_create_uid 和 f_create_time,但 GORM 默认的 DoUpdates 行为(如 DoUpdates(clause.Assignment{...}))容易误包全字段。最稳妥的方式是用 AssignmentColumns 列出白名单字段:
-
f_address、f_update_uid、f_update_time必须出现在AssignmentColumns列表里 -
f_create_uid和f_create_time绝对不能出现,哪怕 struct 里有值也不会被写入 - 如果想让
f_update_time自动设为当前时间,得在 struct 赋值时手动调用time.Now(),GORM 不会自动注入
别依赖 tag 上的 default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP —— Upsert 场景下 MySQL 不触发该机制,必须显式赋值。
MySQL 下自增 ID 会跳号,无法避免但可缓解
执行 INSERT ... ON DUPLICATE KEY UPDATE 时,MySQL 仍会预分配一个自增 ID,即使最终走的是 UPDATE 分支。这意味着连续 Upsert 后再插入新记录,ID 可能从 100 跳到 102。
这不是 GORM 的 bug,是 MySQL 引擎层行为,任何客户端都一样。
如果你的业务强依赖连续 ID(比如对外暴露的编号),得换方案:用非自增主键(如 UUID 或业务生成 ID),或改用先 SELECT 再 UPDATE/INSERT 的两步法(但需加 FOR UPDATE 防并发)。
批量 Upsert 要注意 CreateInBatches 不支持 OnConflict
db.CreateInBatches() 底层仍是普通 INSERT,不兼容 Clauses(clause.OnConflict{...})。想批量 Upsert,只能循环单条 + Create(),或手拼原生 SQL。
更实用的做法是:把数据分片(比如每 100 条一组),每组用 Transaction 包裹,避免单次事务过大;同时确保每组内没有重复冲突键,否则同一事务里多次冲突可能引发死锁。
另外,FullSaveAssociations 在 Upsert 中基本不可用——关联表的 Upsert 需要单独处理,GORM 不会自动递归下推 OnConflict。











