onconflict仅对postgresql有效且需配合唯一约束,否则报错;donothing静默跳过,doupdates需手动指定字段;mysql应改用on duplicate key update。

OnConflict 必须配合唯一约束才有效
单纯调用 OnConflict 不会自动创建唯一索引,它只是生成 SQL 的 ON CONFLICT 子句。如果数据库表上没有对应字段的唯一约束(UNIQUE 或 PRIMARY KEY),PostgreSQL 会直接报错 ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification。
常见错误场景:
- 结构体字段加了
gorm:"unique"标签,但没执行AutoMigrate,表里实际没建约束 - 想按
email冲突,却在OnConflict里传了Columns: []clause.Column{{Name: "user_id"}},列名和约束不匹配 - PostgreSQL 中用了复合唯一索引(如
(customer_id, activity_id)),但OnConflict只写了单个列
验证方式:连接数据库后执行 \d table_name(psql)或查 pg_constraint,确认对应约束存在且名称/字段正确。
DoNothing 和 DoUpdates 的行为差异很关键
DoNothing 是静默跳过整条插入,适合“只允许存在一条”的初始化场景;DoUpdates 则走 DO UPDATE SET,但注意它不会自动合并字段——你得手动指定更新哪些列,且不能写 SET reward_count = reward_count + 1 这种表达式(GORM 默认只支持字面值赋值)。
典型用法:
- 静默插入(存在即跳过):
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) - 冲突时更新指定字段:
db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoUpdates: clause.Assignment{"name": "new name", "updated_at": time.Now()}}).Create(&user) - 想做原子递增?得换招:
db.Exec("INSERT INTO users (email, count) VALUES (?, ?) ON CONFLICT (email) DO UPDATE SET count = users.count + 1", email, 1),纯 SQL 更直接
MySQL 不支持 OnConflict,得用 ON DUPLICATE KEY UPDATE
GORM 的 clause.OnConflict 仅适配 PostgreSQL。MySQL 用户看到 OnConflict 生成的 SQL 会直接报语法错误,因为 MySQL 用的是 ON DUPLICATE KEY UPDATE。
替代方案:
- 用原生 SQL:
db.Exec("INSERT INTO users (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)", id, name) - 或封装成方法,根据
db.Dialector.Name()动态拼接不同方言 - 避免依赖 GORM 的
OnConflict做跨库兼容,它本质是 PostgreSQL 特性封装
另外,MySQL 的 INSERT IGNORE 虽然也能跳过冲突,但它不返回影响行数,无法区分“真插入”和“被忽略”,业务上不如 ON DUPLICATE KEY UPDATE 可控。
批量插入时 OnConflict 容易漏掉 Where 条件
PostgreSQL 允许在 ON CONFLICT 后加 WHERE 子句,用于进一步过滤触发更新的条件(比如只对 status = 'active' 的记录才更新)。但 GORM 的 clause.OnConflict.Where 是个 clause.Where 类型,不是字符串,必须用 clause.Expr 构造:
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
Where: clause.Where{Exprs: []clause.Expression{clause.Expr{SQL: "status = ?", Vars: []interface{}{"active"}}}},
DoUpdates: clause.Assignment{"last_login": time.Now()},
}).CreateInBatches(users, 100)
漏掉这个 Where 可能导致本不该更新的旧记录被覆盖,尤其是做状态机流转时(例如只允许从 pending → done,不能反向)。











