[toc]
复现代码
这个代码的触发条件比较严苛,首先必须要保证gorm执行的一行必须为updates
语句,并且在updates(struct)
,并且传入的这个struct
必须要包含一个直接或者间接关联的一个多态表,这些条件缺一不可
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),但是在什么都不修改的情况下,再次调用接口,更新成功
错误日志如下,是由于一个空指针的调用:
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
导致的错误
func (s *DB) clone() *DB {
db := &DB{
db: s.db, // 从这一行开始panic
}
}
接着往下看,这里的s
应该也是一个nil
// 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
导致调用失败
// TableName return table name
func (scope *Scope) TableName() string {
// ...
return scope.GetModelStruct().TableName(scope.db.Model(scope.Value))
}
从下面的调用,可以看到,实在获取Struct
的结构的时候,由于有多态关联(polymorphic
)的tag,所以需要获取多态表的TableName
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
去获取
// 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
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
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保存下来
// 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()
}
}
}
解决方案
总结
GORM,使用map而不是struct
在使用的GORM的时候,需要更新一些字段的时候最好使用map而不是struct,因为如果使用struct,gorm最终会把这个struct转换为map,并且如果这个struct包含一些关联关系,gorm会一直递归的查找转换下去,如果整个表的关联关系比较复杂,会导致效率比较低下
为什么不需要修改代码,第二次运行就不会panic
这是由于GORM会对struct的结构有一个全局的缓存modelStructsMap
,由于这个是因为查找关联关系的时候报错,其本身已经新建了一个modelstruct
并且缓存了下来,所以再次调用的时候就不会执行后面的代码了
// 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了
今天的文章一个十分边缘的gorm的bug分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17018.html