From 3ae44185f42d8267fe77fb56b2dd879f6d9dbe77 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 5 Jul 2020 11:23:39 +0800 Subject: [PATCH] add ScanList feature for gdb.Result --- container/gvar/gvar_map.go | 5 + container/gvar/gvar_z_unit_struct_test.go | 34 +++ database/gdb/gdb_type_result.go | 230 ++++++++++++++++- database/gdb/gdb_z_driver_test.go | 4 +- database/gdb/gdb_z_init_test.go | 6 +- database/gdb/gdb_z_mysql_internal_test.go | 6 +- database/gdb/gdb_z_mysql_join_test.go | 59 ----- database/gdb/gdb_z_mysql_relation_test.go | 290 ++++++++++++++++++++++ util/gconv/gconv_slice.go | 96 ------- util/gconv/gconv_structs.go | 106 ++++++++ util/gutil/gutil_list.go | 117 +++++---- util/gutil/gutil_z_unit_list_test.go | 17 ++ 12 files changed, 754 insertions(+), 216 deletions(-) delete mode 100644 database/gdb/gdb_z_mysql_join_test.go create mode 100644 database/gdb/gdb_z_mysql_relation_test.go create mode 100644 util/gconv/gconv_structs.go diff --git a/container/gvar/gvar_map.go b/container/gvar/gvar_map.go index 7ac163904..56bace487 100644 --- a/container/gvar/gvar_map.go +++ b/container/gvar/gvar_map.go @@ -13,6 +13,11 @@ func (v *Var) Map(tags ...string) map[string]interface{} { return gconv.Map(v.Val(), tags...) } +// MapStrAny is like function Map, but implements the interface of MapStrAny. +func (v *Var) MapStrAny() map[string]interface{} { + return v.Map() +} + // MapStrStr converts and returns as map[string]string. func (v *Var) MapStrStr(tags ...string) map[string]string { return gconv.MapStrStr(v.Val(), tags...) diff --git a/container/gvar/gvar_z_unit_struct_test.go b/container/gvar/gvar_z_unit_struct_test.go index deb33d0c1..64f7f0d40 100644 --- a/container/gvar/gvar_z_unit_struct_test.go +++ b/container/gvar/gvar_z_unit_struct_test.go @@ -10,6 +10,7 @@ import ( "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/test/gtest" + "github.com/gogf/gf/util/gconv" "testing" ) @@ -40,3 +41,36 @@ func Test_Struct(t *testing.T) { t.Assert(o.Test, -25) }) } + +func Test_Var_Attribute_Struct(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type User struct { + Uid int + Name string + } + user := new(User) + err := gconv.Struct( + g.Map{ + "uid": gvar.New(1), + "name": gvar.New("john"), + }, user) + t.Assert(err, nil) + t.Assert(user.Uid, 1) + t.Assert(user.Name, "john") + }) + gtest.C(t, func(t *gtest.T) { + type User struct { + Uid int + Name string + } + var user *User + err := gconv.Struct( + g.Map{ + "uid": gvar.New(1), + "name": gvar.New("john"), + }, &user) + t.Assert(err, nil) + t.Assert(user.Uid, 1) + t.Assert(user.Name, "john") + }) +} diff --git a/database/gdb/gdb_type_result.go b/database/gdb/gdb_type_result.go index 6160212fc..0e6a4d8b5 100644 --- a/database/gdb/gdb_type_result.go +++ b/database/gdb/gdb_type_result.go @@ -8,14 +8,33 @@ package gdb import ( "database/sql" + "errors" "fmt" "github.com/gogf/gf/container/gvar" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" "math" "reflect" "github.com/gogf/gf/encoding/gparser" ) +// IsEmpty checks and returns whether is empty. +func (r Result) IsEmpty() bool { + return r.Len() == 0 +} + +// Len returns the length of result list. +func (r Result) Len() int { + return len(r) +} + +// Size is alias of function Len. +func (r Result) Size() int { + return r.Len() +} + // Chunk splits an Result into multiple Results, // the size of each array is determined by . // The last chunk may contain less than size elements. @@ -81,7 +100,7 @@ func (r Result) Array(field ...string) []Value { } // MapKeyValue converts to a map[string]Value of which key is specified by . -// Note that the item value can be type of slice. +// Note that the item value may be type of slice. func (r Result) MapKeyValue(key string) map[string]Value { var ( s = "" @@ -227,7 +246,210 @@ func (r Result) Structs(pointer interface{}) (err error) { return nil } -// IsEmpty checks and returns whether is empty. -func (r Result) IsEmpty() bool { - return len(r) == 0 +// ScanList converts to struct slice which contains other complex struct attributes. +// Note that the parameter should be type of *[]struct/*[]*struct. +func (r Result) ScanList(listPointer interface{}, attributeName string, relation ...string) (err error) { + // Necessary checks for parameters. + if attributeName == "" { + return errors.New(`attributeName should not be empty`) + } + if len(relation) > 0 { + if len(relation) < 2 { + return errors.New(`relation name and key should are both necessary`) + } + if relation[0] == "" || relation[1] == "" { + return errors.New(`relation name and key should not be empty`) + } + } + + var ( + reflectValue = reflect.ValueOf(listPointer) + reflectKind = reflectValue.Kind() + ) + if reflectKind == reflect.Interface { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + if reflectKind != reflect.Ptr { + return fmt.Errorf("parameter should be type of *[]struct/*[]*struct, but got: %v", reflectKind) + } + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + if reflectKind != reflect.Slice && reflectKind != reflect.Array { + return fmt.Errorf("parameter should be type of *[]struct/*[]*struct, but got: %v", reflectKind) + } + length := len(r) + if length == 0 { + // The pointed slice is not empty. + if reflectValue.Len() > 0 { + // It here checks if it has struct item, which is already initialized. + // It then returns error to warn the developer its empty and no conversion. + if v := reflectValue.Index(0); v.Kind() != reflect.Ptr { + return sql.ErrNoRows + } + } + // Do nothing for empty struct slice. + return nil + } + var ( + arrayValue reflect.Value // Like: []*Entity + arrayItemType reflect.Type // Like: *Entity + reflectType = reflect.TypeOf(listPointer) + ) + if reflectValue.Len() > 0 { + arrayValue = reflectValue + } else { + arrayValue = reflect.MakeSlice(reflectType.Elem(), length, length) + } + + // Slice element item. + arrayItemType = arrayValue.Index(0).Type() + + // Relation variables. + var ( + relationDataMap map[string]Value + relationFieldName string + relationAttrName string + ) + if len(relation) > 0 { + array := gstr.Split(relation[1], ":") + if len(array) > 1 { + // Defined table field to relation attribute name. + // Like: + // uid:Uid + // uid:UserId + relationFieldName = array[0] + relationAttrName = array[1] + } else { + relationAttrName = relation[1] + // Find the possible map key by given only struct attribute name. + // Like: + // Uid + if k, _ := gutil.MapPossibleItemByKey(r[0].Map(), relation[1]); k != "" { + relationFieldName = k + } + } + if relationFieldName != "" { + relationDataMap = r.MapKeyValue(relationFieldName) + } + if len(relationDataMap) == 0 { + return fmt.Errorf(`cannot find the relation data map, maybe invalid relation key given: %s`, relation[1]) + } + } + // Bind to target attribute. + var ( + ok bool + attrValue reflect.Value + attrKind reflect.Kind + attrType reflect.Type + attrField reflect.StructField + ) + if arrayItemType.Kind() == reflect.Ptr { + if attrField, ok = arrayItemType.Elem().FieldByName(attributeName); !ok { + return fmt.Errorf(`invalid field name: %s`, attributeName) + } + } else { + if attrField, ok = arrayItemType.FieldByName(attributeName); !ok { + return fmt.Errorf(`invalid field name: %s`, attributeName) + } + } + attrType = attrField.Type + attrKind = attrType.Kind() + + // Bind to relation conditions. + var ( + relationValue reflect.Value + relationField reflect.Value + ) + for i := 0; i < arrayValue.Len(); i++ { + arrayElemValue := arrayValue.Index(i) + // The FieldByName should be called on non-pointer reflect.Value. + if arrayElemValue.Kind() == reflect.Ptr { + // Like: []*Entity + arrayElemValue = arrayElemValue.Elem() + if !arrayElemValue.IsValid() { + // The element is nil, then create one and set it to the slice. + // The "reflect.New(itemType.Elem())" creates a new element and returns the address of it. + // For example: + // reflect.New(itemType.Elem()) => *Entity + // reflect.New(itemType.Elem()).Elem() => Entity + arrayElemValue = reflect.New(arrayItemType.Elem()).Elem() + arrayValue.Index(i).Set(arrayElemValue.Addr()) + } + } else { + // Like: []Entity + } + attrValue = arrayElemValue.FieldByName(attributeName) + if len(relation) > 0 { + relationValue = arrayElemValue.FieldByName(relation[0]) + if relationValue.Kind() == reflect.Ptr { + relationValue = relationValue.Elem() + } + } + if len(relationDataMap) > 0 && !relationValue.IsValid() { + return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1]) + } + switch attrKind { + case reflect.Array, reflect.Slice: + if len(relationDataMap) > 0 { + relationField = relationValue.FieldByName(relationAttrName) + if relationField.IsValid() { + if err = gconv.Structs( + relationDataMap[gconv.String(relationField.Interface())], + attrValue.Addr(), + ); err != nil { + return err + } + } else { + // May be the attribute does not exist yet. + return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1]) + } + } else { + return fmt.Errorf(`relationKey should not be empty as field "%s" is slice`, attributeName) + } + + case reflect.Ptr: + e := reflect.New(attrType.Elem()).Elem() + if len(relationDataMap) > 0 { + relationField = relationValue.FieldByName(relationAttrName) + if relationField.IsValid() { + if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil { + return err + } + } else { + // May be the attribute does not exist yet. + return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1]) + } + } else { + if err = gconv.Struct(r[i], e); err != nil { + return err + } + } + attrValue.Set(e.Addr()) + + case reflect.Struct: + e := reflect.New(attrType).Elem() + if len(relationDataMap) > 0 { + relationField = relationValue.FieldByName(relationAttrName) + if relationField.IsValid() { + if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil { + return err + } + } else { + // May be the attribute does not exist yet. + return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1]) + } + } else { + if err = gconv.Struct(r[i], e); err != nil { + return err + } + } + attrValue.Set(e) + + case reflect.Map: + // TODO + } + } + reflect.ValueOf(listPointer).Elem().Set(arrayValue) + return nil } diff --git a/database/gdb/gdb_z_driver_test.go b/database/gdb/gdb_z_driver_test.go index b17bbe35e..a0097c988 100644 --- a/database/gdb/gdb_z_driver_test.go +++ b/database/gdb/gdb_z_driver_test.go @@ -56,8 +56,8 @@ func Test_Custom_Driver(t *testing.T) { gdb.AddConfigNode("driver-test", gdb.ConfigNode{ Host: "127.0.0.1", Port: "3306", - User: "root", - Pass: "12345678", + User: USER, + Pass: PASS, Name: "test", Type: customDriverName, Role: "master", diff --git a/database/gdb/gdb_z_init_test.go b/database/gdb/gdb_z_init_test.go index 2879fe6ab..7ece32965 100644 --- a/database/gdb/gdb_z_init_test.go +++ b/database/gdb/gdb_z_init_test.go @@ -23,6 +23,8 @@ const ( SCHEMA1 = "test1" SCHEMA2 = "test2" PREFIX1 = "gf_" + USER = "john" + PASS = "Nantish1986!" ) var ( @@ -40,8 +42,8 @@ func init() { configNode = gdb.ConfigNode{ Host: "127.0.0.1", Port: "3306", - User: "root", - Pass: "12345678", + User: USER, + Pass: PASS, Name: parser.GetOpt("name", ""), Type: parser.GetOpt("type", "mysql"), Role: "master", diff --git a/database/gdb/gdb_z_mysql_internal_test.go b/database/gdb/gdb_z_mysql_internal_test.go index a25d5391c..dfb561dad 100644 --- a/database/gdb/gdb_z_mysql_internal_test.go +++ b/database/gdb/gdb_z_mysql_internal_test.go @@ -16,6 +16,8 @@ import ( const ( SCHEMA = "test_internal" + USER = "john" + PASS = "Nantish1986!" ) var ( @@ -32,8 +34,8 @@ func init() { configNode = ConfigNode{ Host: "127.0.0.1", Port: "3306", - User: "root", - Pass: "12345678", + User: USER, + Pass: PASS, Name: parser.GetOpt("name", ""), Type: parser.GetOpt("type", "mysql"), Role: "master", diff --git a/database/gdb/gdb_z_mysql_join_test.go b/database/gdb/gdb_z_mysql_join_test.go deleted file mode 100644 index 22790d4a9..000000000 --- a/database/gdb/gdb_z_mysql_join_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2019 gf Author(https://github.com/gogf/gf). 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_test - -import ( - "fmt" - "testing" - - "github.com/gogf/gf/os/gtime" - "github.com/gogf/gf/test/gtest" -) - -// TODO -func Test_Table_Join(t *testing.T) { - var ( - tableUser = "user_" + gtime.TimestampMicroStr() - tableUserDetail = "user_detail_" + gtime.TimestampMicroStr() - ) - if _, err := db.Exec(fmt.Sprintf(` -CREATE TABLE %s ( - uid int(10) unsigned NOT NULL AUTO_INCREMENT, - name varchar(45) NOT NULL, - PRIMARY KEY (uid) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - `, tableUser)); err != nil { - gtest.Error(err) - } - defer dropTable(tableUser) - - if _, err := db.Exec(fmt.Sprintf(` -CREATE TABLE %s ( - uid int(10) unsigned NOT NULL AUTO_INCREMENT, - name varchar(45) NOT NULL, - PRIMARY KEY (uid) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - `, tableUserDetail)); err != nil { - gtest.Error(err) - } - defer dropTable(tableUserDetail) - - gtest.C(t, func(t *gtest.T) { - type EntityUser struct { - Uid int - Name string - } - type EntityUserDetail struct { - Uid int - TrueName string - } - type Entity struct { - User *EntityUser - UserDetail *EntityUserDetail - } - }) -} diff --git a/database/gdb/gdb_z_mysql_relation_test.go b/database/gdb/gdb_z_mysql_relation_test.go new file mode 100644 index 000000000..4f455b888 --- /dev/null +++ b/database/gdb/gdb_z_mysql_relation_test.go @@ -0,0 +1,290 @@ +// Copyright 2019 gf Author(https://github.com/gogf/gf). 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_test + +import ( + "fmt" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" + "testing" + + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/test/gtest" +) + +func Test_Table_Relation(t *testing.T) { + var ( + tableUser = "user_" + gtime.TimestampMicroStr() + tableUserDetail = "user_detail_" + gtime.TimestampMicroStr() + tableUserScores = "user_scores_" + gtime.TimestampMicroStr() + ) + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + uid int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(45) NOT NULL, + PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + uid int(10) unsigned NOT NULL AUTO_INCREMENT, + address varchar(45) NOT NULL, + PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUserDetail)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserDetail) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + uid int(10) unsigned NOT NULL, + score int(10) unsigned NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUserScores)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserScores) + + type EntityUser struct { + Uid int `json:"uid"` + Name string `json:"name"` + } + type EntityUserDetail struct { + Uid int `json:"uid"` + Address string `json:"address"` + } + type EntityUserScores struct { + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + type Entity struct { + User *EntityUser + UserDetail *EntityUserDetail + UserScores []*EntityUserScores + } + + // Initialize the data. + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(tableUser, g.Map{ + "uid": i, + "name": fmt.Sprintf(`name_%d`, i), + }) + gtest.Assert(err, nil) + // Detail. + _, err = db.Insert(tableUserDetail, g.Map{ + "uid": i, + "address": fmt.Sprintf(`address_%d`, i), + }) + gtest.Assert(err, nil) + // Scores. + for j := 1; j <= 5; j++ { + _, err = db.Insert(tableUserScores, g.Map{ + "uid": i, + "score": j, + }) + gtest.Assert(err, nil) + } + } + // MapKeyValue. + gtest.C(t, func(t *gtest.T) { + all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All() + t.Assert(err, nil) + t.Assert(all.Len(), 2) + t.Assert(len(all.MapKeyValue("uid")), 2) + t.Assert(all.MapKeyValue("uid")["3"].Map()["uid"], 3) + t.Assert(all.MapKeyValue("uid")["4"].Map()["uid"], 4) + all, err = db.Table(tableUserScores).Where("uid", g.Slice{3, 4}).Order("id asc").All() + t.Assert(err, nil) + t.Assert(all.Len(), 10) + t.Assert(len(all.MapKeyValue("uid")), 2) + t.Assert(len(all.MapKeyValue("uid")["3"].Slice()), 5) + t.Assert(len(all.MapKeyValue("uid")["4"].Slice()), 5) + t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[0])["uid"], 3) + t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[0])["score"], 1) + t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[4])["uid"], 3) + t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[4])["score"], 5) + }) + // Result ScanList with struct elements and pointer attributes. + gtest.C(t, func(t *gtest.T) { + var users []Entity + // User + all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All() + t.Assert(err, nil) + err = all.ScanList(&users, "User") + t.Assert(err, nil) + t.Assert(len(users), 2) + t.Assert(users[0].User, &EntityUser{3, "name_3"}) + t.Assert(users[1].User, &EntityUser{4, "name_4"}) + // Detail + all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserDetail", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"}) + t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"}) + // Scores + all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserScores", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(len(users[0].UserScores), 5) + t.Assert(len(users[1].UserScores), 5) + t.Assert(users[0].UserScores[0].Uid, 3) + t.Assert(users[0].UserScores[0].Score, 1) + t.Assert(users[0].UserScores[4].Score, 5) + t.Assert(users[1].UserScores[0].Uid, 4) + t.Assert(users[1].UserScores[0].Score, 1) + t.Assert(users[1].UserScores[4].Score, 5) + }) + + // Result ScanList with pointer elements and pointer attributes. + gtest.C(t, func(t *gtest.T) { + var users []*Entity + // User + all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All() + t.Assert(err, nil) + err = all.ScanList(&users, "User") + t.Assert(err, nil) + t.Assert(len(users), 2) + t.Assert(users[0].User, &EntityUser{3, "name_3"}) + t.Assert(users[1].User, &EntityUser{4, "name_4"}) + // Detail + all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserDetail", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"}) + t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"}) + // Scores + all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserScores", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(len(users[0].UserScores), 5) + t.Assert(len(users[1].UserScores), 5) + t.Assert(users[0].UserScores[0].Uid, 3) + t.Assert(users[0].UserScores[0].Score, 1) + t.Assert(users[0].UserScores[4].Score, 5) + t.Assert(users[1].UserScores[0].Uid, 4) + t.Assert(users[1].UserScores[0].Score, 1) + t.Assert(users[1].UserScores[4].Score, 5) + }) + + // Result ScanList with struct elements and struct attributes. + gtest.C(t, func(t *gtest.T) { + type EntityUser struct { + Uid int `json:"uid"` + Name string `json:"name"` + } + type EntityUserDetail struct { + Uid int `json:"uid"` + Address string `json:"address"` + } + type EntityUserScores struct { + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + type Entity struct { + User EntityUser + UserDetail EntityUserDetail + UserScores []EntityUserScores + } + var users []Entity + // User + all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All() + t.Assert(err, nil) + err = all.ScanList(&users, "User") + t.Assert(err, nil) + t.Assert(len(users), 2) + t.Assert(users[0].User, &EntityUser{3, "name_3"}) + t.Assert(users[1].User, &EntityUser{4, "name_4"}) + // Detail + all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserDetail", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"}) + t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"}) + // Scores + all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserScores", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(len(users[0].UserScores), 5) + t.Assert(len(users[1].UserScores), 5) + t.Assert(users[0].UserScores[0].Uid, 3) + t.Assert(users[0].UserScores[0].Score, 1) + t.Assert(users[0].UserScores[4].Score, 5) + t.Assert(users[1].UserScores[0].Uid, 4) + t.Assert(users[1].UserScores[0].Score, 1) + t.Assert(users[1].UserScores[4].Score, 5) + }) + + // Result ScanList with pointer elements and struct attributes. + gtest.C(t, func(t *gtest.T) { + type EntityUser struct { + Uid int `json:"uid"` + Name string `json:"name"` + } + type EntityUserDetail struct { + Uid int `json:"uid"` + Address string `json:"address"` + } + type EntityUserScores struct { + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + type Entity struct { + User EntityUser + UserDetail EntityUserDetail + UserScores []EntityUserScores + } + var users []*Entity + + // User + all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All() + t.Assert(err, nil) + err = all.ScanList(&users, "User") + t.Assert(err, nil) + t.Assert(len(users), 2) + t.Assert(users[0].User, &EntityUser{3, "name_3"}) + t.Assert(users[1].User, &EntityUser{4, "name_4"}) + // Detail + all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserDetail", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"}) + t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"}) + // Scores + all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All() + gtest.Assert(err, nil) + err = all.ScanList(&users, "UserScores", "User", "uid:Uid") + t.Assert(err, nil) + t.Assert(len(users[0].UserScores), 5) + t.Assert(len(users[1].UserScores), 5) + t.Assert(users[0].UserScores[0].Uid, 3) + t.Assert(users[0].UserScores[0].Score, 1) + t.Assert(users[0].UserScores[4].Score, 5) + t.Assert(users[1].UserScores[0].Uid, 4) + t.Assert(users[1].UserScores[0].Score, 1) + t.Assert(users[1].UserScores[4].Score, 5) + }) +} diff --git a/util/gconv/gconv_slice.go b/util/gconv/gconv_slice.go index f7eed76f1..db06fdad1 100644 --- a/util/gconv/gconv_slice.go +++ b/util/gconv/gconv_slice.go @@ -7,11 +7,7 @@ package gconv import ( - "errors" - "fmt" - "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/json" - "reflect" ) // SliceMap is alias of Maps. @@ -127,95 +123,3 @@ func MapsDeep(value interface{}, tags ...string) []map[string]interface{} { return list } } - -// Structs converts any slice to given struct slice. -func Structs(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) { - return doStructs(params, pointer, false, mapping...) -} - -// StructsDeep converts any slice to given struct slice recursively. -func StructsDeep(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) { - return doStructs(params, pointer, true, mapping...) -} - -// doStructs converts any slice to given struct slice. -// -// The parameter should be type of slice. -// -// The parameter should be type of pointer to slice of struct. -// Note that if is a pointer to another pointer of type of slice of struct, -// it will create the struct/pointer internally. -func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...map[string]string) (err error) { - if params == nil { - return errors.New("params cannot be nil") - } - if pointer == nil { - return errors.New("object pointer cannot be nil") - } - defer func() { - // Catch the panic, especially the reflect operation panics. - if e := recover(); e != nil { - err = gerror.NewfSkip(1, "%v", e) - } - }() - pointerRv, ok := pointer.(reflect.Value) - if !ok { - pointerRv = reflect.ValueOf(pointer) - if kind := pointerRv.Kind(); kind != reflect.Ptr { - return fmt.Errorf("pointer should be type of pointer, but got: %v", kind) - } - } - params = Maps(params) - var ( - reflectValue = reflect.ValueOf(params) - reflectKind = reflectValue.Kind() - ) - for reflectKind == reflect.Ptr { - reflectValue = reflectValue.Elem() - reflectKind = reflectValue.Kind() - } - switch reflectKind { - case reflect.Slice, reflect.Array: - // If is an empty slice, no conversion. - if reflectValue.Len() == 0 { - return nil - } - var ( - array = reflect.MakeSlice(pointerRv.Type().Elem(), reflectValue.Len(), reflectValue.Len()) - itemType = array.Index(0).Type() - ) - for i := 0; i < reflectValue.Len(); i++ { - if itemType.Kind() == reflect.Ptr { - // Slice element is type pointer. - e := reflect.New(itemType.Elem()).Elem() - if deep { - if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil { - return err - } - } else { - if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil { - return err - } - } - array.Index(i).Set(e.Addr()) - } else { - // Slice element is not type of pointer. - e := reflect.New(itemType).Elem() - if deep { - if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil { - return err - } - } else { - if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil { - return err - } - } - array.Index(i).Set(e) - } - } - pointerRv.Elem().Set(array) - return nil - default: - return fmt.Errorf("params should be type of slice, but got: %v", reflectKind) - } -} diff --git a/util/gconv/gconv_structs.go b/util/gconv/gconv_structs.go new file mode 100644 index 000000000..f127942ee --- /dev/null +++ b/util/gconv/gconv_structs.go @@ -0,0 +1,106 @@ +// Copyright 2020 gf Author(https://github.com/gogf/gf). 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 gconv + +import ( + "errors" + "fmt" + "github.com/gogf/gf/errors/gerror" + "reflect" +) + +// Structs converts any slice to given struct slice. +func Structs(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) { + return doStructs(params, pointer, false, mapping...) +} + +// StructsDeep converts any slice to given struct slice recursively. +func StructsDeep(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) { + return doStructs(params, pointer, true, mapping...) +} + +// doStructs converts any slice to given struct slice. +// +// The parameter should be type of slice. +// +// The parameter should be type of pointer to slice of struct. +// Note that if is a pointer to another pointer of type of slice of struct, +// it will create the struct/pointer internally. +func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...map[string]string) (err error) { + if params == nil { + return errors.New("params cannot be nil") + } + if pointer == nil { + return errors.New("object pointer cannot be nil") + } + defer func() { + // Catch the panic, especially the reflect operation panics. + if e := recover(); e != nil { + err = gerror.NewfSkip(1, "%v", e) + } + }() + pointerRv, ok := pointer.(reflect.Value) + if !ok { + pointerRv = reflect.ValueOf(pointer) + if kind := pointerRv.Kind(); kind != reflect.Ptr { + return fmt.Errorf("pointer should be type of pointer, but got: %v", kind) + } + } + params = Maps(params) + var ( + reflectValue = reflect.ValueOf(params) + reflectKind = reflectValue.Kind() + ) + for reflectKind == reflect.Ptr { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + switch reflectKind { + case reflect.Slice, reflect.Array: + // If is an empty slice, no conversion. + if reflectValue.Len() == 0 { + return nil + } + var ( + array = reflect.MakeSlice(pointerRv.Type().Elem(), reflectValue.Len(), reflectValue.Len()) + itemType = array.Index(0).Type() + ) + for i := 0; i < reflectValue.Len(); i++ { + if itemType.Kind() == reflect.Ptr { + // Slice element is type pointer. + e := reflect.New(itemType.Elem()).Elem() + if deep { + if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil { + return err + } + } else { + if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil { + return err + } + } + array.Index(i).Set(e.Addr()) + } else { + // Slice element is not type of pointer. + e := reflect.New(itemType).Elem() + if deep { + if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil { + return err + } + } else { + if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil { + return err + } + } + array.Index(i).Set(e) + } + } + pointerRv.Elem().Set(array) + return nil + default: + return fmt.Errorf("params should be type of slice, but got: %v", reflectKind) + } +} diff --git a/util/gutil/gutil_list.go b/util/gutil/gutil_list.go index c1dffdf21..501abc221 100644 --- a/util/gutil/gutil_list.go +++ b/util/gutil/gutil_list.go @@ -13,26 +13,14 @@ import ( // ListItemValues retrieves and returns the elements of all item struct/map with key . // Note that the parameter should be type of slice which contains elements of map or struct, // or else it returns an empty slice. -func ListItemValues(list interface{}, key interface{}) (values []interface{}) { - // If the given is the most common used slice type []map[string]interface{}, - // it enhances the performance using type assertion. - if l, ok := list.([]map[string]interface{}); ok { - if len(l) == 0 { - return - } - if mapKey, ok := key.(string); ok { - if _, ok := l[0][mapKey]; !ok { - return - } - values = make([]interface{}, len(l)) - for k, m := range l { - values[k] = m[mapKey] - } - return - } - } - - // It uses reflect for common checks and converting. +// +// The parameter supports types like: +// []map[string]interface{} +// []map[string]sub-map +// []struct +// []struct:sub-struct +// Note that the sub-map/sub-struct makes sense only if the optional parameter is given. +func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{}) { var ( reflectValue = reflect.ValueOf(list) reflectKind = reflectValue.Kind() @@ -47,47 +35,74 @@ func ListItemValues(list interface{}, key interface{}) (values []interface{}) { if reflectValue.Len() == 0 { return } - var ( - itemValue reflect.Value - givenKeyValue = reflect.ValueOf(key) - ) for i := 0; i < reflectValue.Len(); i++ { - itemValue = reflectValue.Index(i) - // If the items are type of interface{}. - if itemValue.Kind() == reflect.Interface { - itemValue = itemValue.Elem() - } - if itemValue.Kind() == reflect.Ptr { - itemValue = itemValue.Elem() - } - switch itemValue.Kind() { - case reflect.Map: - v := itemValue.MapIndex(givenKeyValue) - if v.IsValid() { - values = append(values, v.Interface()) + if value, ok := doItemValue(reflectValue.Index(i), key); ok { + if len(subKey) > 0 && subKey[0] != nil { + if subValue, ok := doItemValue(value, subKey[0]); ok { + values = append(values, subValue) + } + } else { + values = append(values, value) } - - case reflect.Struct: - // The must be type of string. - v := itemValue.FieldByName(givenKeyValue.String()) - if v.IsValid() { - values = append(values, v.Interface()) - } - default: - return } } - return - default: - return } + return +} + +// ItemValue retrieves and returns its value of which name/attribute specified by . +// The parameter can be type of map/*map/struct/*struct. +func ItemValue(item interface{}, key interface{}) (value interface{}) { + value, _ = doItemValue(item, key) + return +} + +func doItemValue(item interface{}, key interface{}) (value interface{}, found bool) { + var reflectValue reflect.Value + if v, ok := item.(reflect.Value); ok { + reflectValue = v + } else { + reflectValue = reflect.ValueOf(item) + } + reflectKind := reflectValue.Kind() + if reflectKind == reflect.Interface { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + for reflectKind == reflect.Ptr { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + var keyValue reflect.Value + if v, ok := key.(reflect.Value); ok { + keyValue = v + } else { + keyValue = reflect.ValueOf(key) + } + switch reflectKind { + case reflect.Map: + v := reflectValue.MapIndex(keyValue) + if v.IsValid() { + found = true + value = v.Interface() + } + + case reflect.Struct: + // The must be type of string. + v := reflectValue.FieldByName(keyValue.String()) + if v.IsValid() { + found = true + value = v.Interface() + } + } + return } // ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key . // Note that the parameter should be type of slice which contains elements of map or struct, // or else it returns an empty slice. -func ListItemValuesUnique(list interface{}, key string) []interface{} { - values := ListItemValues(list, key) +func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) []interface{} { + values := ListItemValues(list, key, subKey...) if len(values) > 0 { var ( ok bool diff --git a/util/gutil/gutil_z_unit_list_test.go b/util/gutil/gutil_z_unit_list_test.go index 81cc88b9b..20c46421a 100755 --- a/util/gutil/gutil_z_unit_list_test.go +++ b/util/gutil/gutil_z_unit_list_test.go @@ -35,6 +35,23 @@ func Test_ListItemValues_Map(t *testing.T) { }) } +func Test_ListItemValues_SubKey(t *testing.T) { + type Scores struct { + Math int + English int + } + gtest.C(t, func(t *gtest.T) { + listMap := g.List{ + g.Map{"id": 1, "scores": Scores{100, 60}}, + g.Map{"id": 2, "scores": Scores{0, 100}}, + g.Map{"id": 3, "scores": Scores{59, 99}}, + } + t.Assert(gutil.ListItemValues(listMap, "scores", "Math"), g.Slice{100, 0, 59}) + t.Assert(gutil.ListItemValues(listMap, "scores", "English"), g.Slice{60, 100, 99}) + t.Assert(gutil.ListItemValues(listMap, "scores", "PE"), g.Slice{}) + }) +} + func Test_ListItemValues_Struct(t *testing.T) { gtest.C(t, func(t *gtest.T) { type T struct {