Gorm Save update stepping record|Gorm Save primary key conflict|Duplicate entry 'xxxx' for key 'PRIMARY

During my recent field updates using Gorm, I ran into a problem. When I try to update the status field, Gorm prompts me "Duplicate entry 'xxxx' for key 'PRIMARY'" even though the value of the field has not changed.

First, let's look at Savethe description of the method in Gorm's official documentation:

Savemethod saves all fields, even if the field is zero.

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;  

SaveA method is a compound function. It will execute if the saved data does not contain a primary key Create. Conversely, if the saved data contains a primary key, it will execute Update(with all fields).

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  

According to this description, my expected behavior should be an update operation, since I provided the ID field. What actually happens, however, is an insert operation. This confuses me.

In order to understand the problem, I read Gorm's source code deeply:

// 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  
}

The main logic of the source code is as follows:

  1. Get a database instance and prepare to execute SQL statements. valueis the data to operate on.
  2. The type determined using the reflection mechanism value.
  3. If valueit is a Slice or Array, and no conflict resolution strategy ("ON CONFLICT") is defined, then set the conflict resolution strategy that updates all conflicting fields, and perform the insert operation.
  4. If valueit is a Struct, it will try to parse the structure, and then traverse its primary key fields. If the primary key field is zero-valued, an insert is performed.
  5. For types other than Slice, Array, Struct, an update operation will be attempted. If after an update operation, no rows were affected and no specific fields were selected for update, an insert operation is performed.

From this function, we can see that when the corresponding database record passed in valuedoes not exist (judged according to the primary key), Gorm will try to create a new record. Gorm will also try to create a new record if the update operation does not affect any rows.

This behavior is slightly different from the "upsert" (update + insert) logic we usually understand. In this case, Gorm will try to do an insert even though the updated data is exactly the same as in the database. That's why I'm seeing Duplicate entry 'xxxx' for key 'PRIMARY'the error, because that's the primary key violation error message.

I am confused by this behavior of Gorm, and I am also disappointed by the official documentation description, because it does not provide this part of the information.

How to solve this problem?

We can implement a Save method ourselves, using GORM's Create method and conflict resolution strategy:

// 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

In the documentation of Gorm's Create method, we can see this usage. If an ID is provided, it updates all other fields. If no ID is provided, it will insert a new record.

Guess you like

Origin blog.csdn.net/w_monster/article/details/131023367