
本文详解 GORM 中实现“存在则更新、不存在则插入”(即 Upsert)的多种方式,涵盖 GORM v1.20+ 的原生 OnConflict 支持及低版本兼容方案,并对比 FirstOrInit 与 FirstOrCreate 的本质区别。
本文详解 gorm 中实现“存在则更新、不存在则插入”(即 upsert)的多种方式,涵盖 gorm v1.20+ 的原生 `onconflict` 支持及低版本兼容方案,并对比 `firstorinit` 与 `firstorcreate` 的本质区别。
在实际开发中,常需根据唯一标识(如主键或唯一索引字段)判断记录是否存在:若存在则更新指定字段,若不存在则插入新记录。GORM 并未提供直接返回“是否新建”的布尔结果的单一方法,但可通过以下两种主流方式高效、安全地实现该逻辑。
✅ 推荐方案:GORM v1.20+ 原生 Upsert(推荐用于 PostgreSQL/MySQL/SQL Server)
自 v1.20 起,GORM 正式支持跨数据库兼容的 Upsert(即 ON CONFLICT / ON DUPLICATE KEY UPDATE),通过 Clauses(clause.OnConflict{...}) 实现原子化操作,避免竞态条件,性能更优:
import (
"gorm.io/gorm/clause"
)
user := User{ID: 123, Name: "Alice", Age: 30, Email: "alice@example.com"}
result := db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}}, // 冲突检测字段(需为唯一键或主键)
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), // 仅更新指定字段,不覆盖 email 等其他字段
}).Create(&user)
if result.Error != nil {
log.Fatal("Upsert failed:", result.Error)
}
// result.RowsAffected == 1 表示插入,== 0 表示更新(注意:MySQL 返回 2,PostgreSQL 返回 1,行为因驱动而异)
⚠️ 注意事项:
- Columns 必须指定数据库中已定义的唯一约束字段(如 id、email 或联合唯一索引列);
- DoUpdates 中的字段名必须与结构体字段名一致(支持 snake_case 映射);
- SQLite 需启用 sqlite3 扩展并配置 gorm.io/driver/sqlite v1.5.0+;
- SQL Server 使用 MERGE INTO,PostgreSQL 使用 INSERT ... ON CONFLICT,MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE。
⚙️ 兼容方案:GORM v1.9.x 及更早版本(Update-then-Insert)
若无法升级 GORM,推荐采用“先尝试更新,失败后插入”的两步策略,语义清晰且兼容所有版本:
user := User{ID: 123, Name: "Bob", Age: 28}
// 1. 尝试更新指定 ID 的记录
if err := db.Model(&user).Where("id = ?", user.ID).Select("name", "age").Updates(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 2. 记录不存在 → 执行插入
if createErr := db.Create(&user).Error; createErr != nil {
log.Fatal("Create failed:", createErr)
}
} else {
log.Fatal("Update failed:", err)
}
}
✅ 优势:逻辑明确、无数据库兼容性问题;
❗ 风险:非原子操作,在高并发场景下可能因两次查询间隙导致重复插入(需配合唯一索引 + 错误捕获兜底)。
? FirstOrInit vs FirstOrCreate:关键区别
-
FirstOrInit:仅内存初始化,不写入数据库。适用于预填充默认值后再手动决定是否保存:
var user User db.Where("email = ?", "test@example.com").FirstOrInit(&user) // user 已加载(若存在)或用零值/默认值初始化(若不存在),但 DB 无变更 -
FirstOrCreate:查不到则立即插入,并返回创建后的记录:
var user User db.Where("email = ?", "test@example.com").FirstOrCreate(&user, User{Age: 25}) // 若不存在,则插入含 Age=25 的新记录;存在则只查询,不更新其他字段
? 提示:二者均不支持部分字段更新——FirstOrCreate 插入时使用传入的全部默认值,但已有记录不会被修改;如需“存在则更新特定字段”,必须使用上述 Upsert 或 Update-then-Insert 方案。
综上,优先选用 GORM v1.20+ 的 OnConflict 实现 Upsert,兼顾简洁性、原子性与跨库兼容性;旧版本请采用带错误处理的 Update + Create 组合,并确保数据库层具备唯一约束以保障数据一致性。










