Очень маргинальная ошибка горма

Go

[toc]

Воспроизвести код

Условия срабатывания этого кода относительно строгие: во-первых, необходимо убедиться, что строка, выполняемая gorm, должна бытьupdatesзаявление, и вupdates(struct), а входящий thisstructДолжна содержать полиморфную таблицу, которая прямо или косвенно связана, и эти условия обязательны.

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: 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

отладка

Во-первых, глядя вниз с вершины стека паники, это вызов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), поэтому вам нужно получить полиморфную таблицу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"})Условие представляет собой структуру, поэтому будет выполняться следующееcase interface{} -> defaultфилиал. позвоню(&Scope{Value: values}).Fields(), то можно найтиScopeКогда этот объект инициализируется, нетdbЭто поле, поэтому при получении таблицы Когда нужно назвать имяscope.dbтогда паникуй

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
}

На данный момент ошибка закрыта, но давайте посмотрим, почему вызывается эта функция.

Эта функция получит поля, которые необходимо обновить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) {}
}

Этот метод сохранит карту, которую необходимо обновить.

// 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/Enter/G ORM…

Суммировать

GORM, используйте карту вместо структуры

При использовании GORM, когда вам нужно обновить некоторые поляЛучше использовать карту вместо структуры, потому что если используется struct, то gorm со временем преобразует структуру в карту, а если структура содержит какие-то ассоциации, то gorm всегда будет искать и преобразовывать рекурсивно, если ассоциации всей таблицы сложные, то это приведет к низкой эффективности.

Почему не надо модифицировать код, второй запуск не паникует

Это связано с тем, что GORM имеет глобальный кеш для структуры структуры.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Дебаг постоянно ломает точку, выполняет ее снова и снова, и находит весь процесс выполнения, что приводит к игнорированию самого прямого пути поиска кода ошибки.

Однако это тоже ценный опыт.После отладки этого бага,GolandМощная функция отладки уже может воспроизводить сравнение 6