唯一索引冲突需靠数据库UNIQUE约束拦截,而非事务;PostgreSQL用OnConflict需预先建好对应约束且字段名严格匹配,MySQL则须手写ON DUPLICATE KEY UPDATE原生SQL。

唯一索引冲突不能靠事务“防”,得靠数据库约束 + 显式冲突处理逻辑。事务本身不阻止重复插入,只保证原子性;真正拦住重复的是 UNIQUE 约束,而怎么响应冲突,取决于你用什么方式插入。
PostgreSQL 下 OnConflict 必须配唯一约束,否则直接报错
调用 clause.OnConflict 不会自动建索引,它只是生成 SQL 的 ON CONFLICT 子句。如果表上没对应字段的 UNIQUE 或 PRIMARY KEY 约束,PostgreSQL 会立刻返回错误:ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification。
- 常见坑:结构体加了
gorm:"unique",但忘了跑db.AutoMigrate(&User{}),表里实际没建约束 - 列名必须和约束字段完全一致:想按
email冲突,OnConflict里就得写Columns: []clause.Column{{Name: "email"}},写成"user_email"或"Email"都不匹配 - 复合唯一索引(如
(tenant_id, code))要传全字段:[]clause.Column{{Name: "tenant_id"}, {Name: "code"}},只传一个会失败
MySQL 没有 OnConflict,得换语法或换思路
clause.OnConflict 在 MySQL 下完全无效,GORM 生成的 SQL 会直接触发语法错误。MySQL 原生对应的是 ON DUPLICATE KEY UPDATE,但 GORM v2.x 不解析该子句,必须手写原生 SQL。
- 安全写法:
db.Exec("INSERT INTO users (id, name, email) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW()", id, name, email) - 别用
INSERT IGNORE:它静默跳过,但无法区分“插入成功”还是“被忽略”,也拿不到AffectedRows - 字段更新要明确:不能写
ON DUPLICATE KEY UPDATE *,也不能省略等号右边,比如updated_at = NOW()必须写全
关联创建时重复插入 Location/Tag 等实体,光靠事务没用
GORM 的 Create(&msg) 默认对嵌套 Locations 执行级联插入,哪怕 PlaceID 已存在且带 unique 标签,它也不会查库复用,而是硬插——事务只能回滚整个操作,不能“智能跳过”。
- 正确做法是拆开:先禁用自动关联(
db.Session(&gorm.Session{SkipHooks: true}).Create(&msg)),再批量查重已存在的Location记录 - 用
WHERE place_id IN ?一次性捞出所有已存在项,避免 N+1 查询 - 把新旧记录分开,对新记录单独
CreateInBatches,对已有记录用Association().Append()关联
批量插入时 OnConflict 不生效?因为 CreateInBatches 压根不识别 Clauses
db.CreateInBatches() 内部不解析 clause.OnConflict,传进去等于白传。遇到唯一冲突,整批直接中断并报 duplicate key value violates unique constraint。
- 真要批量处理冲突,只能走原生 SQL:
INSERT INTO ... VALUES (...) ON CONFLICT DO NOTHING/DO UPDATE SET ... - VALUES 部分用
$1, $2占位符(PostgreSQL)或?(MySQL),别字符串拼接 - 需要返回结果(比如插入后的 ID 或是否更新)就加
RETURNING id, xmax,再用rows.Scan解析
最易被忽略的一点:无论用哪种方式,UNIQUE 约束必须在数据库层面真实存在,且名字、字段、顺序都和代码里写的对得上。别指望 ORM 自动兜底——它只翻译,不担保约束生效。











