一个十分边缘的gorm的bug

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

[toc]

复现代码

这个代码的触发条件比较严苛,首先必须要保证 gorm 执行的一行必须为updates语句,并且在updates(struct),并且传入的这个struct必须要包含一个直接或者间接关联的一个多态表,这些条件缺一不可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type A struct {
gorm.Model
Name string
B B `gorm:"polymorphic:Owner"`
}

type B struct {
gorm.Model
OwnerID uint
OwnerType string
}

a := A{Name: "test"}
db = db.Model(&a)
db.Where(a)
db.Updates(A{Name: "test2"}) // panic

现象

先说现象,在一次代码联调的过程当中,发现调用一个更新接口的时候会报 500 错误(panic),但是在什么都不修改的情况下,再次调用接口,更新成功

错误日志如下,是由于一个空指针的调用:

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
panic: runtime error: invalid memory address or nil pointer dereference

github.com/jinzhu/gorm.(*DB).clone(0x0, 0x0)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/main.go:715 +0x4e
github.com/jinzhu/gorm.(*DB).Model(0x0, 0x6cb460, 0xc00017c160, 0x0)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/main.go:445 +0x32
github.com/jinzhu/gorm.(*Scope).TableName(0xc000172100, 0xc00016c3c0, 0x6f894a)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/scope.go:325 +0x133
github.com/jinzhu/gorm.(*Scope).GetModelStruct.func2(0xc000170010, 0xc000172100, 0xc00017a050, 0xc0001749c0)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/model_struct.go:420 +0x24c7
github.com/jinzhu/gorm.(*Scope).GetModelStruct(0xc000172100, 0xc00017a050)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/model_struct.go:574 +0x140c
github.com/jinzhu/gorm.(*Scope).Fields(0xc000172100, 0xc000172100, 0x2030000, 0x2030000)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/scope.go:115 +0xaf
github.com/jinzhu/gorm.convertInterfaceToMap(0x6cb460, 0xc00017c160, 0xc00017c001, 0x199)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/scope.go:860 +0x4f8
github.com/jinzhu/gorm.(*Scope).updatedAttrsWithValues(0xc000172080, 0x6cb460, 0xc00017c160, 0x6cb460, 0xc00017c160)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/scope.go:877 +0x8b
github.com/jinzhu/gorm.assignUpdatingAttributesCallback(0xc000172080)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/callback_update.go:25 +0x81
github.com/jinzhu/gorm.(*Scope).callCallbacks(0xc000172080, 0xc00012ff00, 0x9, 0x10, 0xc00017c160)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/scope.go:831 +0x5c
github.com/jinzhu/gorm.(*DB).Updates(0xc00017e090, 0x6cb460, 0xc00017c160, 0x0, 0x0, 0x0, 0xc00017e090)
E:/SoftFile/GOPATH/src/github.com/jinzhu/gorm/main.go:383 +0x13b
main.main()
E:/SoftFile/GOPATH/src/github.com/mohuishou/test/main.go:34 +0x269

调试

首先从 panic 的堆栈顶端往下看, 是调用s.db的时候报的错,推断应该是s的值为nil导致的错误

1
2
3
4
5
func (s *DB) clone() *DB {
db := &DB{
db: s.db, // 从这一行开始panic
}
}

接着往下看,这里的s应该也是一个nil

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
}

接着走, 在获取表名的时候,需要调用scope.db.Model, 这里的 db 应该是一个nil导致调用失败

1
2
3
4
5
// TableName return table name
func (scope *Scope) TableName() string {
// ...
return scope.GetModelStruct().TableName(scope.db.Model(scope.Value))
}

从下面的调用,可以看到,实在获取Struct的结构的时候,由于有多态关联(polymorphic)的 tag,所以需要获取多态表的TableName

1
2
3
4
5
6
7
8
9
10
11
func (scope *Scope) GetModelStruct() *ModelStruct {
// ...
if polymorphic := field.TagSettings["POLYMORPHIC"]; polymorphic != "" {
if value, ok := field.TagSettings["POLYMORPHIC_VALUE"]; ok {
relationship.PolymorphicValue = value
} else {
// 这里调用
relationship.PolymorphicValue = scope.TableName()
}
}
}

调用GetModelStruct的原因是因为需要获取value的所有字段,字段等于 nil 的时候,就会调用GetModelStruct去获取

1
2
3
4
5
6
7
8
9
// Fields get value's fields
func (scope *Scope) Fields() []*Field {
if scope.fields == nil {
// ...
for _, structField := range scope.GetModelStruct().StructFields {
// ...
}
}
}

看这个函数可以发现,会把interface转为map, 由于我们最开始传入的是db.Updates(A{Name: "test2"})条件是一个 struct 所以会执行下面case interface{} -> default分支。此时会调用(&Scope{Value: values}).Fields(),这时候可以发现Scope这个对象在初始话的时候是没有db这个字段的,所以在获取 table name 的时候需要调用到scope.db这时候就会 panic

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
func convertInterfaceToMap(values interface{}, withIgnoredField bool) map[string]interface{} {
var attrs = map[string]interface{}{}

switch value := values.(type) {
case map[string]interface{}:
return value
case []interface{}:
for _, v := range value {
for key, value := range convertInterfaceToMap(v, withIgnoredField) {
attrs[key] = value
}
}
case interface{}:
reflectValue := reflect.ValueOf(values)

switch reflectValue.Kind() {
case reflect.Map:
for _, key := range reflectValue.MapKeys() {
attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface()
}
default:
// 在这里调用
for _, field := range (&Scope{Value: values}).Fields() {
if !field.IsBlank && (withIgnoredField || !field.IsIgnored) {
attrs[field.DBName] = field.Field.Interface()
}
}
}
}
return attrs
}

到这里这个 bug 就算结案了,但是接着看看,为什么会调用这个函数.

这个函数会获取需要更新的字段map,如果传入的是一个struct,会转换为map

1
2
3
4
5
6
7
8
9
func (scope *Scope) updatedAttrsWithValues(value interface{}) (results map[string]interface{}, hasUpdate bool) {
if scope.IndirectValue().Kind() != reflect.Struct {
return convertInterfaceToMap(value, false), true
}

results = map[string]interface{}{}

for key, value := range convertInterfaceToMap(value, true) {}
}

这个方法会把获取到需要更新的 map 保存下来

1
2
3
4
5
6
7
8
9
10
// assignUpdatingAttributesCallback assign updating attributes to model
func assignUpdatingAttributesCallback(scope *Scope) {
if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok {
if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate {
scope.InstanceSet("gorm:update_attrs", updateMaps)
} else {
scope.SkipLeft()
}
}
}

解决方案

https://github.com/jinzhu/gorm/pull/2105

总结

GORM,使用 map 而不是 struct

在使用的 GORM 的时候,需要更新一些字段的时候最好使用 map 而不是 struct,因为如果使用 struct,gorm 最终会把这个 struct 转换为 map,并且如果这个 struct 包含一些关联关系,gorm 会一直递归的查找转换下去,如果整个表的关联关系比较复杂,会导致效率比较低下

为什么不需要修改代码,第二次运行就不会 panic

这是由于 GORM 会对 struct 的结构有一个全局的缓存modelStructsMap,由于这个是因为查找关联关系的时候报错,其本身已经新建了一个modelstruct并且缓存了下来,所以再次调用的时候就不会执行后面的代码了

1
2
3
4
5
6
7
8
9
// GetModelStruct get value's model struct, relationships based on struct and tag definition
func (scope *Scope) GetModelStruct() *ModelStruct {
//...
// Get Cached model struct
if value := modelStructsMap.Get(reflectType); value != nil {
return value
}
// ...
}

调试总结

调试的过程比写来的要艰辛很多,由于调试的时候是从自身的代码开始,通过Goland的 debug 不断的打断点,一遍一遍的执行,查找整个执行的过程,导致忽略了最直接找到错误代码的方式。

不过这也是一个宝贵的经历,这个 Bug 调试结束之后,Goland强大的调试功能已经可以玩的比较 6 了

关注我获取更新

wechat
知乎
github

猜你喜欢


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处,禁止全文转载