refactor(util/gconv): add Converter feature for more flexable and extensible type converting (#4107)

This commit is contained in:
John Guo
2025-03-06 23:04:26 +08:00
committed by GitHub
parent f4074cd815
commit dfe088f5cd
89 changed files with 6871 additions and 4951 deletions

View File

@ -516,8 +516,9 @@ type Core struct {
links *gmap.Map // links caches all created links by node.
logger glog.ILogger // Logger for logging functionality.
config *ConfigNode // Current config node.
localTypeMap *gmap.StrAnyMap // Local type map for database field type conversion.
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
innerMemCache *gcache.Cache
innerMemCache *gcache.Cache // Internal memory cache for storing temporary data.
}
type dynamicConfig struct {
@ -768,6 +769,8 @@ const (
SqlTypeStmtQueryRowContext SqlType = "DB.Statement.QueryRowContext"
)
// LocalType is a type that defines the local storage type of a field value.
// It is used to specify how the field value should be processed locally.
type LocalType string
const (
@ -925,6 +928,7 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) {
links: gmap.New(true),
logger: glog.New(),
config: node,
localTypeMap: gmap.NewStrAnyMap(true),
innerMemCache: gcache.New(),
dynamicConfig: dynamicConfig{
MaxIdleConnCount: node.MaxIdleConnCount,

View File

@ -0,0 +1,82 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gdb
import (
"reflect"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/util/gconv"
)
// iVal is used for type assert api for Val().
type iVal interface {
Val() any
}
var (
// converter is the internal type converter for gdb.
converter = gconv.NewConverter()
)
func init() {
converter.RegisterAnyConverterFunc(
sliceTypeConverterFunc,
reflect.TypeOf([]string{}),
reflect.TypeOf([]float32{}),
reflect.TypeOf([]float64{}),
reflect.TypeOf([]int{}),
reflect.TypeOf([]int32{}),
reflect.TypeOf([]int64{}),
reflect.TypeOf([]uint{}),
reflect.TypeOf([]uint32{}),
reflect.TypeOf([]uint64{}),
)
}
// GetConverter returns the internal type converter for gdb.
func GetConverter() gconv.Converter {
return converter
}
func sliceTypeConverterFunc(from any, to reflect.Value) (err error) {
v, ok := from.(iVal)
if !ok {
return nil
}
fromVal := v.Val()
switch x := fromVal.(type) {
case []byte:
dst := to.Addr().Interface()
err = json.Unmarshal(x, dst)
case string:
dst := to.Addr().Interface()
err = json.Unmarshal([]byte(x), dst)
default:
fromType := reflect.TypeOf(fromVal)
switch fromType.Kind() {
case reflect.Slice:
convertOption := gconv.ConvertOption{
SliceOption: gconv.SliceOption{ContinueOnError: true},
MapOption: gconv.MapOption{ContinueOnError: true},
StructOption: gconv.StructOption{ContinueOnError: true},
}
dv, err := converter.ConvertWithTypeName(fromVal, to.Type().String(), convertOption)
if err != nil {
return err
}
to.Set(reflect.ValueOf(dv))
default:
err = gerror.Newf(
`unsupported type converting from type "%T" to type "%T"`,
fromVal, to,
)
}
}
return err
}

View File

@ -223,6 +223,8 @@ func (c *Core) GetScan(ctx context.Context, pointer interface{}, sql string, arg
case reflect.Struct:
return c.db.GetCore().doGetStruct(ctx, pointer, sql, args...)
default:
}
return gerror.NewCodef(
gcode.CodeInvalidParameter,
@ -735,6 +737,7 @@ func (c *Core) HasTable(name string) (bool, error) {
return false, nil
}
// GetInnerMemCache retrieves and returns the inner memory cache object.
func (c *Core) GetInnerMemCache() *gcache.Cache {
return c.innerMemCache
}

View File

@ -223,7 +223,9 @@ Default:
}
// CheckLocalTypeForField checks and returns corresponding type for given db type.
func (c *Core) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue interface{}) (LocalType, error) {
// The `fieldType` is retrieved from ColumnTypes of db driver, example:
// UNSIGNED INT
func (c *Core) CheckLocalTypeForField(ctx context.Context, fieldType string, _ interface{}) (LocalType, error) {
var (
typeName string
typePattern string
@ -233,7 +235,12 @@ func (c *Core) CheckLocalTypeForField(ctx context.Context, fieldType string, fie
typeName = gstr.Trim(match[1])
typePattern = gstr.Trim(match[2])
} else {
typeName = gstr.Split(fieldType, " ")[0]
var array = gstr.SplitAndTrim(fieldType, " ")
if len(array) > 1 && gstr.Equal(array[0], "unsigned") {
typeName = array[1]
} else if len(array) > 0 {
typeName = array[0]
}
}
typeName = strings.ToLower(typeName)
@ -291,11 +298,6 @@ func (c *Core) CheckLocalTypeForField(ctx context.Context, fieldType string, fie
if typePattern == "1" {
return LocalTypeBool, nil
}
s := gconv.String(fieldValue)
// mssql is true|false string.
if strings.EqualFold(s, "true") || strings.EqualFold(s, "false") {
return LocalTypeBool, nil
}
if gstr.ContainsI(fieldType, "unsigned") {
return LocalTypeUint64Bytes, nil
}

View File

@ -478,8 +478,11 @@ func (c *Core) RowsToResult(ctx context.Context, rows *sql.Rows) (Result, error)
// which will cause struct converting issue.
record[columnTypes[i].Name()] = nil
} else {
var convertedValue interface{}
if convertedValue, err = c.columnValueToLocalValue(ctx, value, columnTypes[i]); err != nil {
var (
convertedValue interface{}
columnType = columnTypes[i]
)
if convertedValue, err = c.columnValueToLocalValue(ctx, value, columnType); err != nil {
return nil, err
}
record[columnTypes[i].Name()] = gvar.New(convertedValue)
@ -498,7 +501,9 @@ func (c *Core) OrderRandomFunction() string {
return "RAND()"
}
func (c *Core) columnValueToLocalValue(ctx context.Context, value interface{}, columnType *sql.ColumnType) (interface{}, error) {
func (c *Core) columnValueToLocalValue(
ctx context.Context, value interface{}, columnType *sql.ColumnType,
) (interface{}, error) {
var scanType = columnType.ScanType()
if scanType != nil {
// Common basic builtin types.

View File

@ -53,7 +53,10 @@ func (r Record) Struct(pointer interface{}) error {
}
return nil
}
return gconv.StructTag(r, pointer, OrmTagForStruct)
return converter.Struct(r, pointer, gconv.StructOption{
PriorityTag: OrmTagForStruct,
ContinueOnError: true,
})
}
// IsEmpty checks and returns whether `r` is empty.

View File

@ -200,5 +200,12 @@ func (r Result) Structs(pointer interface{}) (err error) {
}
return nil
}
return gconv.StructsTag(r, pointer, OrmTagForStruct)
var (
sliceOption = gconv.SliceOption{ContinueOnError: true}
mapOption = gconv.StructOption{
PriorityTag: OrmTagForStruct,
ContinueOnError: true,
}
)
return converter.Structs(r, pointer, sliceOption, mapOption)
}

View File

@ -14,6 +14,15 @@ import (
"github.com/gogf/gf/v2/text/gregex"
)
func Test_GetConverter(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
c := GetConverter()
s, err := c.String(1)
t.AssertNil(err)
t.AssertEQ(s, "1")
})
}
func Test_HookSelect_Regex(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (