GORM避坑指南之含关联关系的更新

6,096 阅读3分钟
原文链接: lailin.xyz

原文地址 lailin.xyz/post/go/gor…

在GORM的文档当中有说明,使用Update, Updates时只会更新改变的字段,但是出现关联关系的时候情况似乎有了一些微妙的变化

If you only want to update changed Fields, you could use Update, Updates

先说结论

db.Model(&user).Update("name", "hello")如果user包含关联关系,user的关联关系将被自动更新

避坑

1.如果确认不会使用到关联相关的回调,可以直接使用UpdateColumn,UpdateColumns方法

下面是来自官方文档的例子:

// Update single attribute, similar with `Update`
db.Model(&user).UpdateColumn("name", "hello")
//// UPDATE users SET name='hello' WHERE id = 111;

// Update multiple attributes, similar with `Updates`
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18 WHERE id = 111;

2.如果需要用到相关的回调,可以手动指定Model里面的结构体

db.Model(&User{Model: Model{ID: 1}}).UpdateColumn("name", "hello")

复现

下面这一段是官方文档中的例子,只会更新更新users表的name字段

// Update single attribute if it is changed
db.Model(&user).Update("name", "hello")
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

但是如果Model(&struct),struct包含关联关系时,struct关联关系将被更新,如以下所示:

type Product struct {
	gorm.Model
	Code         string
	Price        uint
	Applications []Application
}

type Application struct {
	gorm.Model
	Name      string
	ProductID uint
}

func main() {
	// Migrate the schema
	db.AutoMigrate(&Product{}, &Application{})

	// Create
	db.Create(&Product{Code: "L1212", Price: 1000})

	// Read
	var product Product
	db.First(&product, 1) // find product with id 1
	product.Applications = []Application{
		{
			Name: "test",
		},
	}
	// Update - update product's price to 2000
	db.Model(&product).Update("Price", 1000)
    // UPDATE products SET price = '1000', updated_at = '2019-01-29 21:58:52'  WHERE products.deleted_at IS NULL
	// INSERT INTO applications (created_at,updated_at,deleted_at,name,product_id) VALUES ('2019-01-29 21:58:52','2019-01-29 21:58:52',NULL,'test','0')  
}

溯源

该部分默认对GORM的源码有一定的了解

查看Model相关的源码,我们可以发现Model其实就是clone了一个DB对象然后将传入的指针赋值给Value

// Model specify the model you would like to run db operations
//    // update all users's name to `hello`
//    db.Model(&User{}).Update("name", "hello")
//    // if user's primary key is non-blank, will use it as condition, then will only update the user's name to `hello`
//    db.Model(&user).Update("name", "hello")
func (s *DB) Model(value interface{}) *DB {
	c := s.clone()
	c.Value = value
	return c
}

查看Update/Updates相关的源码我们可以发现,这里讲需要更新的字段通过InstanceSet("gorm:update_interface", values)保存了下来

// Update update attributes with callbacks, refer: https://jinzhu.github.io/gorm/crud.html#update
func (s *DB) Update(attrs ...interface{}) *DB {
	return s.Updates(toSearchableMap(attrs...), true)
}

// Updates update attributes with callbacks, refer: https://jinzhu.github.io/gorm/crud.html#update
func (s *DB) Updates(values interface{}, ignoreProtectedAttrs ...bool) *DB {
	return s.NewScope(s.Value).
		Set("gorm:ignore_protected_attrs", len(ignoreProtectedAttrs) > 0).
		InstanceSet("gorm:update_interface", values).
		callCallbacks(s.parent.callbacks.updates).db
}

再看看关联关系更新的代码, 只有一个参数scope,scope哪儿来的呢,上面的s.NewScope(s.Value),这个地方其实也是将最开始Model中的 value拷贝了一份

func saveAfterAssociationsCallback(scope *Scope) {
    // 判断是否存在关系关系然后更新bala bala...
    for _, field := range scope.Fields() {
		autoUpdate, autoCreate, saveReference, relationship := saveAssociationCheck(scope, field)
        //...
    }
    //...
}

看到这个了差不多就可明白了,主要原因是因为Modelvalue一直跟随到了最后,导致最后执行关联关系更新回调的时候,检测到有关联数据数据表中不存在,就会自然的根据关联关系插入进去

相关资源

  1. 目前还不清楚这是一个bug还是一个feature,先提交了一个issue: github.com/jinzhu/gorm…
  2. 一个十分边缘的gorm的bug