Gorm Save更新踩坑记录|Gorm Save主键冲突|Duplicate entry ‘xxxx‘ for key ‘PRIMARY

news/2025/3/20 6:30:52/

在我最近使用Gorm进行字段更新的过程中,我遇到了一个问题。当我尝试更新status字段时,即使该字段的值没有发生变化,Gorm还是提示我“Duplicate entry ‘xxxx’ for key ‘PRIMARY’”。

首先,让我们看看Gorm的官方文档对Save方法的描述:

Save方法会保存所有的字段,即使字段是零值。

db.First(&user)  user.Name = "jinzhu 2"  
user.Age = 100  
db.Save(&user)  
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;  

Save方法是一个复合函数。如果保存的数据不包含主键,它将执行Create。反之,如果保存的数据包含主键,它将执行Update(带有所有字段)。

db.Save(&User{Name: "jinzhu", Age: 100})  
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")  db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})  
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1  

根据这个描述,我预期的行为应该是更新操作,因为我提供了ID字段。然而,实际发生的却是插入操作。这让我感到困惑。

为了理解这个问题,我深入阅读了Gorm的源码:

// Save updates value in database. If value doesn't contain a matching primary key, value is inserted.func (db *DB) Save(value interface{}) (tx *DB) {  
tx = db.getInstance()  
tx.Statement.Dest = value  reflectValue := reflect.Indirect(reflect.ValueOf(value))  
for reflectValue.Kind() == reflect.Ptr || reflectValue.Kind() == reflect.Interface {  
reflectValue = reflect.Indirect(reflectValue)  
}  switch reflectValue.Kind() {  
case reflect.Slice, reflect.Array:  
if _, ok := tx.Statement.Clauses["ON CONFLICT"]; !ok {  
tx = tx.Clauses(clause.OnConflict{UpdateAll: true})  
}  
tx = tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time", true))  
case reflect.Struct:  
if err := tx.Statement.Parse(value); err == nil && tx.Statement.Schema != nil {  
for _, pf := range tx.Statement.Schema.PrimaryFields {  
if _, isZero := pf.ValueOf(tx.Statement.Context, reflectValue); isZero {  
return tx.callbacks.Create().Execute(tx)  
}  
}  
}  fallthrough  
default:  
selectedUpdate := len(tx.Statement.Selects) != 0  
// when updating, use all fields including those zero-value fields  
if !selectedUpdate {  
tx.Statement.Selects = append(tx.Statement.Selects, "*")  
}  updateTx := tx.callbacks.Update().Execute(tx.Session(&Session{Initialized: true}))  if updateTx.Error == nil && updateTx.RowsAffected == 0 && !updateTx.DryRun && !selectedUpdate {  
return tx.Create(value)  
}  return updateTx  
}  return  
}

源码的主要逻辑如下:

  1. 获取数据库实例并准备执行SQL语句。value是要操作的数据。
  2. 利用反射机制确定value的类型。
  3. 如果value是Slice或Array,并且没有定义冲突解决策略(“ON CONFLICT”),那么设置更新所有冲突字段的冲突解决策略,并执行插入操作。
  4. 如果value是一个Struct,那么会尝试解析这个结构体,然后遍历它的主键字段。如果主键字段是零值,则执行插入操作。
  5. 对于除Slice、Array、Struct以外的类型,将尝试执行更新操作。如果在更新操作后,没有任何行受到影响,并且没有选择特定的字段进行更新,则执行插入操作。

从这个函数我们可以看出,当传入的value对应的数据库记录不存在时(根据主键判断),Gorm会尝试创建一个新的记录。如果更新操作不影响任何行,Gorm同样会尝试创建一个新的记录。

这个行为与我们通常理解的“upsert”(update + insert)逻辑略有不同。在这种情况下,即使更新的数据与数据库中的数据完全相同,Gorm还是会尝试进行插入操作。这就是为什么我会看到Duplicate entry 'xxxx' for key 'PRIMARY'的错误,因为这就是主键冲突的错误提示。

对于Gorm的这种行为我感到困惑,同时我也对官方文档的描述感到失望,因为它并没有提供这部分的信息。

如何解决这个问题呢?

我们可以自己实现一个Save方法,利用GORM的Create方法和冲突解决策略:

// Update all columns to new value on conflict except primary keys and those columns having default values from sql func  
db.Clauses(clause.OnConflict{  UpdateAll: true,  
}).Create(&users)  
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;  
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL

在Gorm的Create方法的文档中,我们可以看到这种用法。如果提供了ID,它会更新其他所有的字段。如果没有提供ID,它会插入新的记录。


http://www.ppmy.cn/news/192373.html

相关文章

centos上搭建以太坊私有链

第一步 安装go语言环境 root用户下新建一个soft目录(相关目录可以随意) 去golang官网找到链接,在soft目录下下载golang压缩包 wget --no-check-certificate https://go.dev/dl/go1.20.4.linux-amd64.tar.gz其中如果出现Unable to establish SSL connection.&am…

涉及float和double

文章目录 涉及float和double的问题:它们的存储方式:有效位? 链式结构 涉及float和double的问题: 它们的存储方式: 它们会分成小数部分和指数部分分别存储。小数部分的有效位数越多,精度就越高,…

dell服务器显示器接口在哪里,【Dell S2719H 显示器使用总结】安装|接口|边框|背板_摘要频道_什么值得买...

Dell S2719H 显示器使用总结(安装|接口|边框|背板) Dell S2719H的安装首先是需要将支架与底座相连,底座的旋钮,可以在无需螺丝刀的帮助下实现支架的固定,贴心且便捷。 接下来,仅需要将Dell S2719H的主体的背部插槽与支架顶端相连即可,想要取下则需要借助工具。 完成安装之…

开源高星精选,10个2023企业级Python测试项目,再不学习时间就没了

纸上得来终觉浅,光学习理论知识是不够的。 想要学好软件测试必须要结合实战项目深入掌握,今天给大家分享十个2022最新企业级Python软件测试项目: ​ 添加图片注释,不超过 140 字(可选) ▌Rank 1&#xf…

计算机网络和因特网概述

一.计算机网络和因特网 计算机网络是指多台计算机通过通信线路或其他连接方式相互连接起来,实现信息交换和共享的系统。而因特网是利用标准化协议互联起来的全球性计算机网络,是计算机网络的一种特殊形式。 1.1 什么是因特网 1.1.1 从具体构成描述 因…

佳能清零软件万能版_Mac版Fujifilm X Webcam发布:用无反相机做网络摄像头

>>>2020 苹果教育优惠来袭,购买就送 AirPods!点击进入苹果教育商店。(注意:若 App 端无法直接点开链接,请尝试长按) 富士(Fujifilm)今天发布了适用于 macOS 系统的软件-Fujifilm X Webcam&#…

pcie1 4 速度_APS-C画幅速度旗舰 富士X-T3单机身售9399元

X-T系列作为富士X无反相机目前的旗舰机型,经过几年的发展已经获得不少专业用户的好评,全新的X-T3微单相机并非仅仅只是参数上的提升,更重要的是从内到外整体性能有翻天覆地的变化,同时扩展更广的摄影领域,X系列历经三代…

plsa原理

LSI:Latent Semantic Indexing LSI就是LSI 以上只是同一個模型的不同称谓 PLSI:Probabilistic Latent Semantic Indexing PLSI就是PLSA 以上只是同一個模型的不同称谓 PLSA的原文是: 《Probabilistic Latent Semantic Indexing》 这个论文有两个版本, …