From 02478371f2e716c82f78f17b90d986d7b24c52f1 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 28 Sep 2021 22:02:08 +0800 Subject: [PATCH] fix issue missing org tag value retrieving when using struct in Fields for package gdb --- database/gdb/gdb_core.go | 2 +- database/gdb/gdb_func.go | 20 +++++++ database/gdb/gdb_model_fields.go | 5 +- database/gdb/gdb_z_init_test.go | 13 ++++ database/gdb/gdb_z_mysql_model_test.go | 79 ++++++++++++++++--------- internal/structs/structs_field.go | 69 ++++++++++++++++++--- internal/structs/structs_tag.go | 13 +++- internal/structs/structs_z_unit_test.go | 20 +++++++ 8 files changed, 178 insertions(+), 43 deletions(-) diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index f291dbe1f..1a9082d97 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -667,7 +667,7 @@ func (c *Core) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`%+v`, c)), nil } -// writeSqlToLogger outputs the sql object to logger. +// writeSqlToLogger outputs the Sql object to logger. // It is enabled only if configuration "debug" is true. func (c *Core) writeSqlToLogger(ctx context.Context, sql *Sql) { var transactionIdStr string diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 6b18b2c26..d7eae4f80 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -328,6 +328,26 @@ func doQuoteString(s, charLeft, charRight string) string { return gstr.Join(array1, ",") } +func getFieldsFromStructOrMap(structOrMap interface{}) (fields []string) { + fields = []string{} + if utils.IsStruct(structOrMap) { + structFields, _ := structs.Fields(structs.FieldsInput{ + Pointer: structOrMap, + RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, + }) + for _, structField := range structFields { + if tag := structField.Tag(OrmTagForStruct); tag != "" && gregex.IsMatchString(regularFieldNameRegPattern, tag) { + fields = append(fields, tag) + } else { + fields = append(fields, structField.Name()) + } + } + } else { + fields = gutil.Keys(structOrMap) + } + return +} + // GetWhereConditionOfStruct returns the where condition sql and arguments by given struct pointer. // This function automatically retrieves primary or unique field and its attribute value as condition. func GetWhereConditionOfStruct(pointer interface{}) (where string, args []interface{}, err error) { diff --git a/database/gdb/gdb_model_fields.go b/database/gdb/gdb_model_fields.go index 3b60c2a21..fa9e503c2 100644 --- a/database/gdb/gdb_model_fields.go +++ b/database/gdb/gdb_model_fields.go @@ -13,7 +13,6 @@ import ( "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" - "github.com/gogf/gf/util/gutil" ) // Fields appends `fieldNamesOrMapStruct` to the operation fields of the model, multiple fields joined using char ','. @@ -43,7 +42,7 @@ func (m *Model) Fields(fieldNamesOrMapStruct ...interface{}) *Model { )) default: return m.appendFieldsByStr(gstr.Join( - m.mappingAndFilterToTableFields(gutil.Keys(r), true), ",", + m.mappingAndFilterToTableFields(getFieldsFromStructOrMap(r), true), ",", )) } } @@ -71,7 +70,7 @@ func (m *Model) FieldsEx(fieldNamesOrMapStruct ...interface{}) *Model { case []string: model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields(r, true), ",") default: - model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields(gutil.Keys(r), true), ",") + model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields(getFieldsFromStructOrMap(r), true), ",") } return model } diff --git a/database/gdb/gdb_z_init_test.go b/database/gdb/gdb_z_init_test.go index 9e73bf49c..143760e1e 100644 --- a/database/gdb/gdb_z_init_test.go +++ b/database/gdb/gdb_z_init_test.go @@ -33,6 +33,7 @@ var ( db gdb.DB dbPrefix gdb.DB dbCtxStrict gdb.DB + dbInvalid gdb.DB configNode gdb.ConfigNode ) @@ -62,9 +63,13 @@ func init() { nodeCtxStrict := configNode nodeCtxStrict.CtxStrict = true + nodeInvalid := configNode + nodeInvalid.Port = "3307" + gdb.AddConfigNode("test", configNode) gdb.AddConfigNode("prefix", nodePrefix) gdb.AddConfigNode("ctxstrict", nodeCtxStrict) + gdb.AddConfigNode("nodeinvalid", nodeInvalid) gdb.AddConfigNode(gdb.DefaultGroupName, configNode) // Default db. @@ -109,6 +114,14 @@ func init() { gtest.Error(err) } dbCtxStrict.SetSchema(TestSchema1) + + // Invalid db. + if r, err := gdb.New("nodeinvalid"); err != nil { + gtest.Error(err) + } else { + dbInvalid = r + } + dbInvalid.SetSchema(TestSchema1) } func createTable(table ...string) string { diff --git a/database/gdb/gdb_z_mysql_model_test.go b/database/gdb/gdb_z_mysql_model_test.go index 7dee91bde..dbb594ca4 100644 --- a/database/gdb/gdb_z_mysql_model_test.go +++ b/database/gdb/gdb_z_mysql_model_test.go @@ -7,10 +7,13 @@ package gdb_test import ( + "bytes" "context" "database/sql" "fmt" "github.com/gogf/gf/encoding/gjson" + "github.com/gogf/gf/text/gstr" + "os" "testing" "time" @@ -1086,35 +1089,53 @@ func Test_Model_Structs(t *testing.T) { }) } -// JSON tag is only used for JSON Marshal/Unmarshal, DO NOT use it in multiple purposes! -//func Test_Model_StructsWithJsonTag(t *testing.T) { -// table := createInitTable() -// defer dropTable(table) -// -// db.SetDebug(true) -// gtest.C(t, func(t *gtest.T) { -// type User struct { -// Uid int `json:"id"` -// Passport string -// Password string -// Name string `json:"nick_name"` -// Time gtime.Time `json:"create_time"` -// } -// var users []User -// err := db.Model(table).Order("id asc").Scan(&users) -// if err != nil { -// gtest.Error(err) -// } -// t.Assert(len(users), TableSize) -// t.Assert(users[0].Uid, 1) -// t.Assert(users[1].Uid, 2) -// t.Assert(users[2].Uid, 3) -// t.Assert(users[0].Name, "name_1") -// t.Assert(users[1].Name, "name_2") -// t.Assert(users[2].Name, "name_3") -// t.Assert(users[0].Time.String(), "2018-10-24 10:00:00") -// }) -//} +func Test_Model_StructsWithOrmTag(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + dbInvalid.SetDebug(true) + defer dbInvalid.SetDebug(false) + gtest.C(t, func(t *gtest.T) { + type User struct { + Uid int `orm:"id"` + Passport string + Password string `orm:"password"` + Name string `orm:"nick_name"` + Time gtime.Time `orm:"create_time"` + } + var ( + users []User + buffer = bytes.NewBuffer(nil) + ) + dbInvalid.GetLogger().SetWriter(buffer) + defer dbInvalid.GetLogger().SetWriter(os.Stdout) + dbInvalid.Model(table).Order("id asc").Scan(&users) + t.Assert( + gstr.Contains(buffer.String(), "SELECT `id`,`Passport`,`password`,`nick_name`,`create_time` FROM `user"), + true, + ) + }) + + //db.SetDebug(true) + //defer db.SetDebug(false) + gtest.C(t, func(t *gtest.T) { + type A struct { + Passport string + Password string + } + type B struct { + A + NickName string + } + one, err := db.Model(table).Fields(&B{}).Where("id", 2).One() + t.AssertNil(err) + t.Assert(len(one), 3) + t.Assert(one["nickname"], "name_2") + t.Assert(one["passport"], "user_2") + t.Assert(one["password"], "pass_2") + }) + +} func Test_Model_Scan(t *testing.T) { table := createInitTable() diff --git a/internal/structs/structs_field.go b/internal/structs/structs_field.go index f72bc83e6..e241355ce 100644 --- a/internal/structs/structs_field.go +++ b/internal/structs/structs_field.go @@ -75,6 +75,15 @@ const ( RecursiveOptionEmbeddedNoTag = 2 // Recursively retrieving fields as map if the field is an embedded struct and the field has no tag. ) +type FieldsInput struct { + // Pointer should be type of struct/*struct. + Pointer interface{} + + // RecursiveOption specifies the way retrieving the fields recursively if the attribute + // is an embedded struct. It is RecursiveOptionNone in default. + RecursiveOption int +} + type FieldMapInput struct { // Pointer should be type of struct/*struct. Pointer interface{} @@ -88,6 +97,52 @@ type FieldMapInput struct { RecursiveOption int } +// Fields retrieves and returns the fields of `pointer` as slice. +func Fields(in FieldsInput) ([]*Field, error) { + var ( + fieldFilterMap = make(map[string]struct{}) + retrievedFields = make([]*Field, 0) + ) + rangeFields, err := getFieldValues(in.Pointer) + if err != nil { + return nil, err + } + for i := 0; i < len(rangeFields); i++ { + field := rangeFields[i] + if _, ok := fieldFilterMap[field.Name()]; ok { + continue + } + // Only retrieve exported attributes. + if !field.IsExported() { + continue + } + if field.IsEmbedded() { + if in.RecursiveOption != RecursiveOptionNone { + switch in.RecursiveOption { + case RecursiveOptionEmbeddedNoTag: + if field.TagStr() != "" { + break + } + fallthrough + case RecursiveOptionEmbedded: + structFields, err := Fields(FieldsInput{ + Pointer: field.Value, + RecursiveOption: in.RecursiveOption, + }) + if err != nil { + return nil, err + } + rangeFields = append(rangeFields, structFields...) + } + } + continue + } + fieldFilterMap[field.Name()] = struct{}{} + retrievedFields = append(retrievedFields, field) + } + return retrievedFields, nil +} + // FieldMap retrieves and returns struct field as map[name/tag]*Field from `pointer`. // // The parameter `pointer` should be type of struct/*struct. @@ -99,8 +154,8 @@ type FieldMapInput struct { // is an embedded struct. // // Note that it only retrieves the exported attributes with first letter up-case from struct. -func FieldMap(input FieldMapInput) (map[string]*Field, error) { - fields, err := getFieldValues(input.Pointer) +func FieldMap(in FieldMapInput) (map[string]*Field, error) { + fields, err := getFieldValues(in.Pointer) if err != nil { return nil, err } @@ -114,7 +169,7 @@ func FieldMap(input FieldMapInput) (map[string]*Field, error) { continue } tagValue = "" - for _, p := range input.PriorityTagArray { + for _, p := range in.PriorityTagArray { tagValue = field.Tag(p) if tagValue != "" && tagValue != "-" { break @@ -125,8 +180,8 @@ func FieldMap(input FieldMapInput) (map[string]*Field, error) { if tagValue != "" { mapField[tagValue] = tempField } else { - if input.RecursiveOption != RecursiveOptionNone && field.IsEmbedded() { - switch input.RecursiveOption { + if in.RecursiveOption != RecursiveOptionNone && field.IsEmbedded() { + switch in.RecursiveOption { case RecursiveOptionEmbeddedNoTag: if field.TagStr() != "" { mapField[field.Name()] = tempField @@ -136,8 +191,8 @@ func FieldMap(input FieldMapInput) (map[string]*Field, error) { case RecursiveOptionEmbedded: m, err := FieldMap(FieldMapInput{ Pointer: field.Value, - PriorityTagArray: input.PriorityTagArray, - RecursiveOption: input.RecursiveOption, + PriorityTagArray: in.PriorityTagArray, + RecursiveOption: in.RecursiveOption, }) if err != nil { return nil, err diff --git a/internal/structs/structs_tag.go b/internal/structs/structs_tag.go index f7d8c85c7..da56e4a47 100644 --- a/internal/structs/structs_tag.go +++ b/internal/structs/structs_tag.go @@ -13,6 +13,7 @@ import ( ) // ParseTag parses tag string into map. +// For example, tag ParseTag(`v:"required" p:"id" d:"1"`) => map[v:required p:id d:1]. func ParseTag(tag string) map[string]string { var ( key string @@ -68,7 +69,9 @@ func ParseTag(tag string) map[string]string { // // The parameter `pointer` should be type of struct/*struct. // -// Note that it only retrieves the exported attributes with first letter up-case from struct. +// Note that, +// 1. It only retrieves the exported attributes with first letter up-case from struct. +// 2. The parameter `priority` should be given, it only retrieves fields that has given tag. func TagFields(pointer interface{}, priority []string) ([]*Field, error) { return getFieldValuesByTagPriority(pointer, priority, map[string]struct{}{}) } @@ -77,7 +80,9 @@ func TagFields(pointer interface{}, priority []string) ([]*Field, error) { // // The parameter `pointer` should be type of struct/*struct. // -// Note that it only retrieves the exported attributes with first letter up-case from struct. +// Note that, +// 1. It only retrieves the exported attributes with first letter up-case from struct. +// 2. The parameter `priority` should be given, it only retrieves fields that has given tag. func TagMapName(pointer interface{}, priority []string) (map[string]string, error) { fields, err := TagFields(pointer, priority) if err != nil { @@ -93,7 +98,9 @@ func TagMapName(pointer interface{}, priority []string) (map[string]string, erro // TagMapField retrieves struct tags as map[tag]*Field from `pointer`, and returns it. // The parameter `object` should be either type of struct/*struct/[]struct/[]*struct. // -// Note that it only retrieves the exported attributes with first letter up-case from struct. +// Note that, +// 1. It only retrieves the exported attributes with first letter up-case from struct. +// 2. The parameter `priority` should be given, it only retrieves fields that has given tag. func TagMapField(object interface{}, priority []string) (map[string]*Field, error) { fields, err := TagFields(object, priority) if err != nil { diff --git a/internal/structs/structs_z_unit_test.go b/internal/structs/structs_z_unit_test.go index 825d095bb..3a6e3daf0 100644 --- a/internal/structs/structs_z_unit_test.go +++ b/internal/structs/structs_z_unit_test.go @@ -102,6 +102,26 @@ func Test_StructOfNilPointer(t *testing.T) { }) } +func Test_Fields(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Name string `params:"name"` + Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` + } + var user *User + fields, _ := structs.Fields(user) + t.Assert(len(fields), 3) + t.Assert(fields[0].Name(), "Id") + t.Assert(fields[1].Name(), "Name") + t.Assert(fields[1].Tag("params"), "name") + t.Assert(fields[2].Name(), "Pass") + t.Assert(fields[2].Tag("my-tag1"), "pass1") + t.Assert(fields[2].Tag("my-tag2"), "pass2") + t.Assert(fields[2].Tag("params"), "pass") + }) +} + func Test_FieldMap(t *testing.T) { gtest.C(t, func(t *gtest.T) { type User struct {