gorm 的 onconflict 不等于原生 on duplicate key update,因其默认生成 postgresql 语法(on conflict do update),在 mysql 下直接报错;需改用 clause.insert{modifier: "on duplicate key update"} 并手动拼写 values() 字段更新逻辑,且依赖表存在唯一约束、字段顺序一致、collation 匹配才能生效。

为什么 GORM 的 OnConflict 不等于原生 ON DUPLICATE KEY UPDATE
GORM v2 的 OnConflict 默认生成的是 INSERT ... ON CONFLICT DO UPDATE(PostgreSQL 语法),不是 MySQL 的 ON DUPLICATE KEY UPDATE。直接调用 db.Clauses(clause.OnConflict{...}).Create(...) 在 MySQL 下会报错 ERROR 1064 (42000): You have an error in your SQL syntax。
必须显式切换为 MySQL 兼容模式:
- 用
db.Clauses(clause.OnConflict{DoUpdates: clause.AssignmentColumns(...)}).Create(...)仍不行——它还是走 PostgreSQL 路径 - 正确做法是弃用
OnConflict,改用db.Clauses(clause.Insert{Modifier: "ON DUPLICATE KEY UPDATE"}).Create(...) - 或者更稳妥:手写原生 SQL +
db.Exec(),避免 GORM 自动拼接逻辑干扰
db.Clauses(clause.Insert{Modifier: "ON DUPLICATE KEY UPDATE"}) 怎么写字段更新
这个写法只是告诉 GORM 加上后缀,但不会自动帮你映射 VALUES(column)。字段更新逻辑得自己拼进 Modifier 字符串里,例如:
db.Clauses(clause.Insert{
Modifier: "ON DUPLICATE KEY UPDATE email = VALUES(email), updated_at = VALUES(updated_at)",
}).Create(&user)
注意点:
-
VALUES(email)是 MySQL 内置函数,取本次INSERT中对应字段的值,不是 Go 变量 - 不能写成
email = ?或email = user.Email,GORM 不会做变量替换 - 如果要自增字段(如
login_count = login_count + 1),直接写表达式即可,不用VALUES() - 批量插入时,每个
VALUES()都绑定到当前行,无需额外处理
唯一索引冲突没触发更新?检查这三处
常见现象:执行了带 ON DUPLICATE KEY UPDATE 的语句,但数据没更新,也没有报错,新行也没插入。
原因通常在表结构或语句本身:
- 表里没有定义任何
PRIMARY KEY或UNIQUE索引——ON DUPLICATE KEY UPDATE完全不生效 - 冲突字段用了大小写不敏感的 collation(如
utf8mb4_0900_as_cs是大小写敏感,utf8mb4_general_ci不是),导致'ABC'和'abc'被认为相同,但更新时又因 collation 规则匹配失败 - SQL 中
INSERT的字段列表和VALUES值顺序不一致,或漏写了冲突字段(比如唯一索引是(a,b),但只在VALUES里传了a)
自增 ID 跳跃和并发安全怎么权衡
MySQL 的 ON DUPLICATE KEY UPDATE 在检测到冲突时,仍会预分配一个自增 ID,即使最终执行的是更新。这意味着:
- 连续插入相同唯一键,ID 会跳着增长(如 1→3→5),这不是 bug,是 MySQL 的实现机制
- 高并发下多个事务同时尝试插入同一唯一键,可能触发间隙锁(gap lock),造成阻塞甚至死锁
- 如果业务强依赖 ID 连续性(如对外暴露的订单号),别用这个方案;改用先
SELECT ... FOR UPDATE再INSERT/UPDATE手动控制 - 若只关心幂等写入,接受 ID 跳跃,就别加额外锁——
ON DUPLICATE KEY UPDATE本身就是原子的,比应用层加锁更轻量
真正容易被忽略的是:这个语句的「影响行数」返回值含义。返回 1 表示插入,2 表示更新,0 表示更新但所有字段值未变。别只看是否报错,要读返回值才能确认行为。











