gorm 的 onconflict 严格依赖数据库唯一约束,无约束则报错或静默失败;columns 必须匹配已建的唯一索引字段,doupdates 仅更新指定字段,结构体不会自动刷新需额外查询。

直接说结论:GORM 的 OnConflict 不是“写个条件就能用”,它严格依赖数据库层的唯一约束,没建对约束,OnConflict 会静默失败或报错,不是 GORM 的 bug,是设计前提。
OnConflict 报 “no unique or exclusion constraint” 怎么办
这是 PostgreSQL 最常见的报错,MySQL 对应的是 ERROR 1062 (23000): Duplicate entry 但没这么直白。根本原因不是 SQL 写错了,而是数据库表里压根没有对应字段的唯一约束。
- 用
\d table_name(PostgreSQL)或SHOW CREATE TABLE table_name(MySQL)确认目标列是否标有UNIQUE或PRIMARY KEY -
ON CONFLICT (id)要求id列上存在显式定义的唯一约束,不能只靠业务逻辑“我们认为它唯一” - 复合场景(比如按
user_id和event_type联合去重)必须建复合唯一索引:ALTER TABLE events ADD CONSTRAINT events_user_action_key UNIQUE (user_id, event_type) - SQLite 需启用
sqlite3扩展并使用gorm.io/driver/sqlitev1.5.0+,否则ON CONFLICT不生效
GORM v1.20+ 中 OnConflict 的 Columns 和 DoUpdates 怎么配
Columns 和 DoUpdates 是两个独立控制点,配错会导致语义错乱或全量覆盖。
-
Columns: []clause.Column{{Name: "id"}}必须填数据库中已存在的唯一约束字段名(注意大小写和映射),不能填结构体字段别名如"ID"或"user_id"(除非你显式gorm:"column:user_id"且约束也建在该列上) -
DoUpdates: clause.AssignmentColumns([]string{"name", "updated_at"})指定要更新的字段列表,只更新这些字段;未列出的字段(如created_at、email)保持原值不变 - 如果想更新所有非主键字段,用
clause.AssignmentColumns(nil),但要注意这会覆盖零值(比如Age: 0会被写入) -
DoUpdates不支持表达式,比如不能写updated_at = NOW(),得靠数据库默认值或触发器
Upsert 后结构体字段没刷新?这是正常行为
db.Clauses(...).Create(&user) 成功后,user 结构体里的字段不会自动同步数据库最新值,包括自增主键(如果没设)、默认值、updated_at 等。
- 例如你设了
updated_at time.Time `gorm:"default:current_timestamp"`,Upsert 更新后user.UpdatedAt仍是旧值 - 需要最新数据,必须额外查一次:
db.First(&user, user.ID) - 如果只关心是否新建,看
result.RowsAffected:PostgreSQL 返回1(插入或更新都算 1),MySQL 返回1(插入)或2(更新),不能跨数据库统一判断 - 不要依赖
FirstOrCreate替代OnConflict:它不是原子操作,高并发下可能插入重复记录
最容易被忽略的其实是约束与字段名的严格匹配——OnConflict 不做推断,只做反射,错一个字母、少一个约束、大小写不一致,它就拒绝工作,而且不报明确错误,只让逻辑跑飞。











