震惊!同事小张踩了 gorm 神奇的 Scan 函数的坑

go 刘宇帅 4年前 阅读量: 10940

gorm 简介

gorm 是 go 语言中实现的比较好的 ORM 包,且是国人开发的。项目地址

事故描述

Scan 是 gorm 提供的一个把数据库结果读取到 struct 的函数。定义如下:

// Scan scan value to a struct
func (s *DB) Scan(dest interface{}) *DB {
    return s.NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callbacks.queries).db
}

今天同事小张写代码的时候写了一个类似如下的语句:

var account models.Account

database.DB.Table("accounts").Scan(&account) // 这里 DB 是 gorm 的实例

如上查出了表 accounts 里所有的数据但是把结果读入一个 struct 变量 account 中,显然这是错的,但是 gorm 竟然可以正常运行,最诡异的是读出来的 account 信息是乱掉的,通过 account 的 id 查到数据库中的数据的 name 字段和 account 实际返回的不一致。由于小张真实的查询语句比上面的复杂很多,所以当时小张是懵逼了的,而当小张拿给我看的时候我也是懵逼了的。
当我看到 Scan 这里明显应该使用 First 代替的时候,我就想到应该是 Scan 的实现造成的。

Scan 源码

callback_query.go

        if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
            defer rows.Close()

            columns, _ := rows.Columns()
            for rows.Next() {
                scope.db.RowsAffected++

                elem := results
                if isSlice {
                    elem = reflect.New(resultType).Elem()
                }

                scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())

                if isSlice {
                    if isPtr {
                        results.Set(reflect.Append(results, elem.Addr()))
                    } else {
                        results.Set(reflect.Append(results, elem))
                    }
                }
            }

            if err := rows.Err(); err != nil {
                scope.Err(err)
            } else if scope.db.RowsAffected == 0 && !isSlice {
                scope.Err(ErrRecordNotFound)
            }
        }

以上代码是读取数据结果并把结果放入目标变量 results (即对应我们上面声明的 account),以上代码是个循环,循环处理数据库返回的多条记录。最核心的代码是如上的 8 - 13 行,这里判断如果 results 是 slice 那么就 new 一个 元素,并在 13 行把当前行数据库数据赋值给新元素,然后在 15 到 20 行时 append slice 后面,如果 results 不是 slice 那么 再 第 13 行处理任何一行数据库数据时都是重复的对 results 赋值。所以 Scan 并不像 gorm 注释中说的给 struct 赋值,其是可以传 slice 和 struct 的。
但是为什么小张传的 account 的信息是乱掉的呢?继续追第 13 行代码如下:

func (scope *Scope) scan(rows *sql.Rows, columns []string, fields []*Field) {
    var (
        ignored            interface{}
        values             = make([]interface{}, len(columns))
        selectFields       []*Field
        selectedColumnsMap = map[string]int{}
        resetFields        = map[int]*Field{}
    )

    for index, column := range columns {
        values[index] = &ignored

        selectFields = fields
        offset := 0
        if idx, ok := selectedColumnsMap[column]; ok {
            offset = idx + 1
            selectFields = selectFields[offset:]
        }

        for fieldIndex, field := range selectFields {
            if field.DBName == column {
                if field.Field.Kind() == reflect.Ptr {
                    values[index] = field.Field.Addr().Interface()
                } else {
                    reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type))
                    reflectValue.Elem().Set(field.Field.Addr())
                    values[index] = reflectValue.Interface()
                    resetFields[index] = field
                }

                selectedColumnsMap[column] = offset + fieldIndex

                if field.IsNormal {
                    break
                }
            }
        }
    }

    scope.Err(rows.Scan(values...))

    for index, field := range resetFields {
        fmt.Println(reflect.ValueOf(values[index]).Elem().Elem().String())
        if v := reflect.ValueOf(values[index]).Elem().Elem(); v.IsValid() {
            field.Field.Set(v)
        }
    }
}

第 65 到 93 行是把数据库返回的字段和我们要保存到的 struct 的个字段关系一一对应起来分别保存在 values 和 resetFields 中,第 97 到 102 行是把数据库个字段数据赋值到 struct 对应的字段值。而最重要的是第 99 到 101 这里会遍历每行数据的每个字段时如果该字段不为空(这就是为啥数据会乱掉)就会把该字段值更新到 struct 中。到这我们就可以明白当我们调用 Scan 时传入 struct 而数据库返回多条数据时,如果第一条数据有为空的字段而后面数据行有该字段不为空时就会覆盖掉 struct 中对应字段的值。

总结

Scan 的这个问题应该算是 gorm 的一个 bug,而从 gorm 源码结构上可以看到引入这么奇怪的问题是因为 gorm 为了复用数据库数据赋值到目标变量上引入的。不只是 Scan 存在这个问题,Find 也同样存在同样的问题。但其实按理说我们应该乖乖的使用 First,这也不能算是一个 bug 吧。

提示

功能待开通!


暂无评论~