[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()
}
}
}
решение
Суммировать
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