IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    【Gorm】Save 方法更新踩坑记录

    whrss (whrss9527@gmail.com)发表于 2023-06-03 16:53:55
    love 0

    在我最近使用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`(带有所有字段)。
    
    
    
    ```go
    
    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,它会插入新的记录。


    经热心老哥提醒,当使用 INSERT ... ON DUPLICATE KEY UPDATE 语句时,如果插入的行因为唯一索引或主键冲突而失败,MySQL 会执行更新操作。这种情况下,会涉及到锁的问题,如果多个事务同时试图对同一行进行 INSERT ... ON DUPLICATE KEY UPDATE 操作,可能会引起死锁,尤其是在复杂的查询和多表操作中。死锁发生时,MySQL 会自动检测并回滚其中一个事务来解决死锁。频繁使用这种语句在高并发场景下可能会对性能造成影响,因为每次冲突都需要进行额外的更新操作,并且涉及到锁的管理。

    所以,如果你的业务涉及复杂和并发的业务场景,可以尝试手动在应用层进行检测冲突,避免数据库的冲突和锁的竞争。



沪ICP备19023445号-2号
友情链接