注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
[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 dereferencegithub.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 了
关注我获取更新猜你喜欢