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

在 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方法

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

1
2
3
4
5
6
7
// 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里面的结构体

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

复现

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

1
2
3
// 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关联关系将被更新,如以下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
5
6
7
8
9
10
// 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)保存了下来

1
2
3
4
5
6
7
8
9
10
11
12
// 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拷贝了一份

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

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

相关资源

  1. 目前还不清楚这是一个 bug 还是一个 feature,先提交了一个 issue: https://github.com/jinzhu/gorm/issues/2278
  2. 一个十分边缘的 gorm 的 bug
  • 本文作者: mohuishou <1@lailin.xyz>
  • 本文链接: https://lailin.xyz/post/60163.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!