一个十分边缘的gorm的bug

一个十分边缘的gorm的bug这个代码的触发条件比较严苛,首先必须要保证gorm执行的一行必须为updates语句,并且在updates(struct),并且传入的这个struct必须要包含一个直接或者间接关联的一个多态表,这些条件缺一不可 先说现象,在一次代码联调的过程当中,发现调用一个更新接口的时候会报…

[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()
        }
    }
}

解决方案

github.com/jinzhu/gorm…

总结

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

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注