用 clause.OnConflict 实现原子 Upsert 是复杂更新中最可靠的方式,但必须确保冲突字段有唯一约束,否则静默失效;FirstOrCreate 因非原子性在高并发下易导致主键冲突或数据覆盖,且无法控制更新字段、无数据库层原子性保证。

直接说结论:用 clause.OnConflict 实现原子 Upsert 是复杂更新中最可靠的方式,但必须确保冲突字段有唯一约束,否则静默失效。
为什么不能只靠 FirstOrCreate 做“存在则更新”
它本质是两条 SQL:先 SELECT 判断是否存在,再决定 INSERT 或 UPDATE。在高并发下,两次查询之间可能被其他事务插入同一条记录,导致主键冲突或数据覆盖。
-
FirstOrCreate返回的是新查出或新建的实例,不反映实际执行了插入还是更新 - 无法控制“更新哪些字段”,默认全量更新(包括零值),容易误覆写
- 没有数据库层原子性保证,
SELECT + INSERT/UPDATE之间存在竞态窗口
clause.OnConflict 的实际参数组合要点
不同数据库底层语法不同(MySQL 用 ON DUPLICATE KEY UPDATE,PostgreSQL 用 ON CONFLICT),但 GORM 统一收口到 clause.OnConflict。关键不是“怎么写”,而是“字段怎么配”。
-
Columns必须填数据库中真实存在的唯一索引列,比如[]clause.Column{{Name: "email"}}—— 如果email字段没建唯一索引,该子句完全不生效,且无报错 -
DoUpdates推荐用clause.AssignmentColumns显式指定字段,避免把CreatedAt、ID等不该更新的字段也写进去 - MySQL 驱动下
RowsAffected可能返回 2(表示 insert + update 各算一次),而 PostgreSQL 返回 1,不能依赖它判断操作类型
软删除场景下 clause.Where 和 clause.Locking 要一起用
当模型启用了 gorm.DeletedAt 软删除,又需要对“逻辑删除但未物理删除”的记录做条件更新(比如恢复状态),仅靠 Unscoped() 不够安全 —— 它会绕过所有 scope,包括你自定义的权限过滤。
- 正确做法是显式拼
clause.Where:db.Clauses(clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "deleted_at", Value: nil}}}).Where("status = ?", "disabled").Update("status", "enabled") - 若该更新需强一致性(如订单状态变更),必须加锁:
db.Clauses(clause.Locking{Strength: "UPDATE"}).Where(...).Update(...),否则并发时可能多次触发同一状态迁移 -
clause.Locking在 SQLite 中不支持,MySQL/PostgreSQL 可用,SQL Server 需用MERGE替代
真正容易被忽略的是:Clause 不是“写完就生效”的魔法开关,它只是往 Statement.Clauses 里塞结构体,最终是否参与 SQL 构建,取决于后续调用的方法是否触发对应 Build 阶段 —— 比如 Create 会走 Build("INSERT"),但 Update 默认不触发 OnConflict,必须搭配 Create 使用。


















