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

在我最近使用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,它会插入新的记录。

猜你喜欢

转载自blog.csdn.net/w_monster/article/details/131023367