ecodeclub / eorm

简单 ORM 框架
Apache License 2.0
194 stars 64 forks source link

valuer/reflect: 用 Field 取代 FieldByName #81

Closed flycash closed 2 years ago

flycash commented 2 years ago

仅限中文

使用场景

在 ORM 的底层实现里面,解析结构体的值有两种实现,一种是 unsafe,一种是 reflect。显然 unsafe 大多数时候性能会很好,尤其是在组合的情况下。

但是目前的 reflect 的实现也没有达到最佳状态,例如:

// Field 返回字段值
func (r reflectValue) Field(name string) (any, error) {
    res := r.val.FieldByName(name)
    if res == (reflect.Value{}) {
        return nil, errs.NewInvalidFieldError(name)
    }
    return res.Interface(), nil
}

func (r reflectValue) SetColumns(rows *sql.Rows) error {
    cs, err := rows.Columns()
    if err != nil {
        return err
    }
    if len(cs) > len(r.meta.Columns) {
        return errs.ErrTooManyColumns
    }

    // TODO 性能优化
    // colValues 和 colEleValues 实质上最终都指向同一个对象
    colValues := make([]interface{}, len(cs))
    colEleValues := make([]reflect.Value, len(cs))
    for i, c := range cs {
        cm, ok := r.meta.ColumnMap[c]
        if !ok {
            return errs.NewInvalidColumnError(c)
        }
        val := reflect.New(cm.Typ)
        colValues[i]=val.Interface()
        colEleValues[i] = val.Elem()
    }
    if err = rows.Scan(colValues...); err != nil {
        return err
    }

    for i, c := range cs {
        cm := r.meta.ColumnMap[c]
        fd := r.val.FieldByName(cm.FieldName)
        fd.Set(colEleValues[i])
    }
    return nil
}

都使用了 FieldByName,但是我们有更好的选择 Field

我对 FieldFieldByName 两个做了性能测试,效果如下:

func BenchmarkFieldIndexOrName(b *testing.B) {
    tm := TestModel{}
    val := reflect.ValueOf(tm)
    b.Run("by index", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            // 随便取一个,差异不大
            _ = val.Field(3)
        }
    })

    b.Run("by name", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            // 随便取一个,差异不大
            _ = val.FieldByName("Age")
        }
    })
}

在我电脑上执行测试 go test -bench=BenchmarkFieldIndexOrName -benchmem -benchtime=10000x,得到的结果是:

BenchmarkFieldIndexOrName/by_index-12              10000                 2.840 ns/op           0 B/op          0 allocs/op      
BenchmarkFieldIndexOrName/by_name-12               10000                71.07 ns/op            8 B/op          1 allocs/op      
PASS

可以看到,by index 远比 by name 快。

所以为了进一步提高性能,我希望能够将这两个地方的 FieldByName 都替换为 Field

为了达到这个目的,还需要修改我们的 TableMeta ,在字段里面增加一个 index 字段。暂时不考虑组合情况,我们可以把它定义成:

// ColumnMeta represents model's field, or column
type ColumnMeta struct {
    ColumnName string
    FieldName    string
    Typ             reflect.Type
    IsPrimaryKey    bool
    IsAutoIncrement bool
    // Offset 是字段偏移量。需要注意的是,这里的字段偏移量是相对于整个结构体的偏移量
    // 例如在组合的情况下,
    // type A struct {
    //     name string
    //     B
    // }
    // type B struct {
    //     age int
    // }
    // age 的偏移量是相对于 A 的起始地址的偏移量
    Offset uintptr
    // IsHolderType 用于表达是否是 Holder 的类型
    // 所谓的 Holder,就是指同时实现了 sql.Scanner 和 driver.Valuer 两个接口的类型
    IsHolderType bool

        Index int
}

那么在解析 ColumnMeta 的时候,同时要把这个 Index 设置好。

要求: