
本文详解 GORM 中实现原子化 upsert 操作的两种主流方式:GORM v1.20+ 的原生 OnConflict 语法(跨数据库兼容),以及低版本(v1.9.x 及以下)推荐的“先更新后插入”策略,并对比 FirstOrInit 与 FirstOrCreate 的本质区别。
本文详解 gorm 中实现原子化 upsert 操作的两种主流方式:gorm v1.20+ 的原生 `onconflict` 语法(跨数据库兼容),以及低版本(v1.9.x 及以下)推荐的“先更新后插入”策略,并对比 `firstorinit` 与 `firstorcreate` 的本质区别。
在实际开发中,常需执行“若记录已存在则更新指定字段,否则插入新记录”的原子操作(即 Upsert)。GORM 并未提供直接命名为 Upsert 的方法,但自 v1.20 起通过 Clauses(clause.OnConflict{...}) 提供了标准化、数据库无关的实现方案;而对旧版本,则需采用显式控制流程来保障一致性。
✅ 推荐方案:GORM v1.20+ 使用 OnConflict(推荐用于生产环境)
该方式利用各数据库原生的 upsert 语义(如 PostgreSQL 的 ON CONFLICT、MySQL 的 ON DUPLICATE KEY UPDATE、SQL Server 的 MERGE),由数据库保证原子性与并发安全:
import (
"gorm.io/gorm/clause"
)
user := User{ID: 123, Name: "Alice", Age: 28, Email: "alice@example.com"}
// 当主键(或唯一索引)"id" 冲突时,仅更新 Name 和 Age 字段
result := db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}}, // 触发冲突的列(必须是主键或有唯一约束)
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), // 仅更新这两列
}).Create(&user)
if result.Error != nil {
log.Fatal("Upsert failed:", result.Error)
}
// user.ID 始终为 123;若为新建,user.ID 不变;若为更新,user 结构体字段不会被自动刷新(需显式 SELECT)⚠️ 注意事项:
- Columns 中指定的字段必须具备唯一约束(UNIQUE)或为主键(PRIMARY KEY),否则数据库将报错;
- DoUpdates: clause.AssignmentColumns(...) 表示仅更新列出的字段,其余字段(如 Email)保持原值不变;
- Create() 调用后,结构体不会自动回填数据库最新值(例如更新时间戳或默认值),如需最新数据,请额外执行 db.First(&user, user.ID)。
⚙️ 兼容方案:GORM v1.9.x 及以下 —— “先更新,后创建”
当无法升级 GORM 时,应避免竞态条件(如 FirstOrCreate 后再 Save 可能引发重复插入),推荐以下安全模式:
user := User{ID: 123, Name: "Bob", Age: 30}
// 1. 尝试更新:仅修改目标字段
res := db.Model(&user).Where("id = ?", user.ID).Updates(map[string]interface{}{
"name": user.Name,
"age": user.Age,
})
// 2. 若更新影响行为 0(即无匹配记录),则创建新记录
if res.RowsAffected == 0 {
if err := db.Create(&user).Error; err != nil {
log.Fatal("Insert failed:", err)
}
} else if res.Error != nil {
log.Fatal("Update failed:", res.Error)
}此方案虽非单条 SQL 原子执行,但在事务包裹下可确保强一致性:
tx := db.Begin()
defer func() {
if r := recover(); r != nil || tx.Error != nil {
tx.Rollback()
}
}()
if err := tx.Model(&user).Where("id = ?", user.ID).Updates(...).Error; err != nil {
tx.Rollback()
return err
}
if tx.RowsAffected == 0 {
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
}
return tx.Commit().Error❗ 关键辨析:FirstOrInit vs FirstOrCreate
- FirstOrInit:仅内存初始化。若数据库无匹配记录,它会用查询条件填充 struct(如 db.Where("id = ?", 123).FirstOrInit(&u) → u.ID = 123),但不写入数据库,后续需手动 Create()。
- FirstOrCreate:查库 + 条件创建。若未找到,会根据 Where 条件和 Attrs/Assign 构造新记录并 INSERT;但它不支持部分字段更新,且无法区分“刚创建”还是“已存在”,不适合本场景。
因此,二者均不适用于“存在则更新指定字段”需求——它们的设计目标是“按条件获取或初始化单条记录”,而非 upsert。
✅ 总结建议
| 场景 | 推荐方式 |
|---|---|
| GORM ≥ v1.20,追求原子性与性能 | Clauses(OnConflict{...}).Create() |
| GORM < v1.20 或需精细控制逻辑 | 事务内 Update(...).RowsAffected == 0 ? Create() : nil |
| 需要严格区分“新建”与“更新”状态 | 在 OnConflict 后结合 result.RowsAffected 判断(PostgreSQL/MySQL 返回值语义略有差异,建议以业务日志或额外 SELECT 辅助识别) |
始终为数据库字段添加必要唯一约束,并在关键路径启用事务,是构建可靠 upsert 逻辑的基石。

















