From 685bf56a30ad987d18c2e4e6be284bfb5397d08a Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 3 Aug 2021 22:21:20 +0800 Subject: [PATCH] fix issue #1325 --- database/gdb/gdb_model_with.go | 19 ++- .../gdb/gdb_z_mysql_association_with_test.go | 125 +++++++++++++++++- internal/structs/structs_field.go | 61 +++++++-- internal/structs/structs_z_unit_test.go | 12 +- util/gvalid/gvalid_validator_check_struct.go | 6 +- 5 files changed, 202 insertions(+), 21 deletions(-) diff --git a/database/gdb/gdb_model_with.go b/database/gdb/gdb_model_with.go index f2bae7b1e..52ae7d617 100644 --- a/database/gdb/gdb_model_with.go +++ b/database/gdb/gdb_model_with.go @@ -63,7 +63,11 @@ func (m *Model) doWithScanStruct(pointer interface{}) error { err error allowedTypeStrArray = make([]string, 0) ) - fieldMap, err := structs.FieldMap(pointer, nil, false) + fieldMap, err := structs.FieldMap(structs.FieldMapInput{ + Pointer: pointer, + PriorityTagArray: nil, + RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, + }) if err != nil { return err } @@ -79,7 +83,7 @@ func (m *Model) doWithScanStruct(pointer interface{}) error { fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]") withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]") ) - // It does select operation if the field type is in the specified with type array. + // It does select operation if the field type is in the specified "with" type array. if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 { allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr) } @@ -94,6 +98,7 @@ func (m *Model) doWithScanStruct(pointer interface{}) error { if parsedTagOutput.With == "" { continue } + // Just handler "with" type attribute struct. if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) { continue } @@ -120,8 +125,8 @@ func (m *Model) doWithScanStruct(pointer interface{}) error { if relatedFieldValue == nil { return gerror.NewCodef( gerror.CodeInvalidParameter, - `cannot find the related value for attribute name "%s" of with tag "%s"`, - relatedAttrName, parsedTagOutput.With, + `cannot find the related value of attribute name "%s" in with tag "%s" for attribute "%s.%s"`, + relatedAttrName, parsedTagOutput.With, reflect.TypeOf(pointer).Elem(), field.Name(), ) } bindToReflectValue := field.Value @@ -173,7 +178,11 @@ func (m *Model) doWithScanStructs(pointer interface{}) error { err error allowedTypeStrArray = make([]string, 0) ) - fieldMap, err := structs.FieldMap(pointer, nil, false) + fieldMap, err := structs.FieldMap(structs.FieldMapInput{ + Pointer: pointer, + PriorityTagArray: nil, + RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, + }) if err != nil { return err } diff --git a/database/gdb/gdb_z_mysql_association_with_test.go b/database/gdb/gdb_z_mysql_association_with_test.go index 5a102fa12..d02316e41 100644 --- a/database/gdb/gdb_z_mysql_association_with_test.go +++ b/database/gdb/gdb_z_mysql_association_with_test.go @@ -874,7 +874,7 @@ PRIMARY KEY (id) }) } -func Test_Table_Relation_WithAll_Embedded(t *testing.T) { +func Test_Table_Relation_WithAll_Embedded_With_SelfMaintained_Attributes(t *testing.T) { var ( tableUser = "user" tableUserDetail = "user_detail" @@ -989,6 +989,129 @@ PRIMARY KEY (id) }) } +func Test_Table_Relation_WithAll_Embedded_Without_SelfMaintained_Attributes(t *testing.T) { + var ( + tableUser = "user" + tableUserDetail = "user_detail" + tableUserScores = "user_scores" + ) + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +name varchar(45) NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %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 IF NOT EXISTS %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 UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + } + + type UserScores struct { + gmeta.Meta `orm:"table:user_scores"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + // For Test Only + type UserEmbedded struct { + Id int `json:"id"` + Name string `json:"name"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + *UserDetail `orm:"with:uid=id"` + UserEmbedded + UserScores []*UserScores `orm:"with:uid=id"` + } + + // Initialize the data. + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(tableUser, g.Map{ + "id": 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) + } + } + db.SetDebug(true) + defer db.SetDebug(false) + + gtest.C(t, func(t *gtest.T) { + var user *User + err := db.Model(tableUser).WithAll().Where("id", 3).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 3) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 3) + t.Assert(user.UserDetail.Address, `address_3`) + t.Assert(len(user.UserScores), 5) + t.Assert(user.UserScores[0].Uid, 3) + t.Assert(user.UserScores[0].Score, 1) + t.Assert(user.UserScores[4].Uid, 3) + t.Assert(user.UserScores[4].Score, 5) + }) + gtest.C(t, func(t *gtest.T) { + var user User + err := db.Model(tableUser).WithAll().Where("id", 4).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 4) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 4) + t.Assert(user.UserDetail.Address, `address_4`) + t.Assert(len(user.UserScores), 5) + t.Assert(user.UserScores[0].Uid, 4) + t.Assert(user.UserScores[0].Score, 1) + t.Assert(user.UserScores[4].Uid, 4) + t.Assert(user.UserScores[4].Score, 5) + }) +} + func Test_Table_Relation_WithAll_Embedded_WithoutMeta(t *testing.T) { var ( tableUser = "user" diff --git a/internal/structs/structs_field.go b/internal/structs/structs_field.go index 81ae1d960..1af783adf 100644 --- a/internal/structs/structs_field.go +++ b/internal/structs/structs_field.go @@ -29,6 +29,11 @@ func (f *Field) IsEmbedded() bool { return f.Field.Anonymous } +// TagStr returns the tag string of the field. +func (f *Field) TagStr() string { + return string(f.Field.Tag) +} + // IsExported returns true if the given field is exported. func (f *Field) IsExported() bool { return f.Field.PkgPath == "" @@ -64,6 +69,25 @@ func (f *Field) OriginalKind() reflect.Kind { return kind } +const ( + RecursiveOptionNone = 0 // No recursively retrieving fields as map if the field is an embedded struct. + RecursiveOptionEmbedded = 1 // Recursively retrieving fields as map if the field is an embedded struct. + RecursiveOptionEmbeddedNoTag = 2 // Recursively retrieving fields as map if the field is an embedded struct and the field has no tag. +) + +type FieldMapInput struct { + // Pointer should be type of struct/*struct. + Pointer interface{} + + // PriorityTagArray specifies the priority tag array for retrieving from high to low. + // If it's given `nil`, it returns map[name]*Field, of which the `name` is attribute name. + PriorityTagArray []string + + // RecursiveOption specifies the way retrieving the fields recursively if the attribute + // is an embedded struct. It is RecursiveOptionNone in default. + RecursiveOption int +} + // FieldMap retrieves and returns struct field as map[name/tag]*Field from `pointer`. // // The parameter `pointer` should be type of struct/*struct. @@ -75,8 +99,8 @@ func (f *Field) OriginalKind() reflect.Kind { // is an embedded struct. // // Note that it only retrieves the exported attributes with first letter up-case from struct. -func FieldMap(pointer interface{}, priority []string, recursive bool) (map[string]*Field, error) { - fields, err := getFieldValues(pointer) +func FieldMap(input FieldMapInput) (map[string]*Field, error) { + fields, err := getFieldValues(input.Pointer) if err != nil { return nil, err } @@ -90,7 +114,7 @@ func FieldMap(pointer interface{}, priority []string, recursive bool) (map[strin continue } tagValue = "" - for _, p := range priority { + for _, p := range input.PriorityTagArray { tagValue = field.Tag(p) if tagValue != "" && tagValue != "-" { break @@ -101,15 +125,28 @@ func FieldMap(pointer interface{}, priority []string, recursive bool) (map[strin if tagValue != "" { mapField[tagValue] = tempField } else { - if recursive && field.IsEmbedded() { - m, err := FieldMap(field.Value, priority, recursive) - if err != nil { - return nil, err - } - for k, v := range m { - if _, ok := mapField[k]; !ok { - tempV := v - mapField[k] = tempV + if input.RecursiveOption != RecursiveOptionNone && field.IsEmbedded() { + switch input.RecursiveOption { + case RecursiveOptionEmbeddedNoTag: + if field.TagStr() != "" { + mapField[field.Name()] = tempField + break + } + fallthrough + case RecursiveOptionEmbedded: + m, err := FieldMap(FieldMapInput{ + Pointer: field.Value, + PriorityTagArray: input.PriorityTagArray, + RecursiveOption: input.RecursiveOption, + }) + if err != nil { + return nil, err + } + for k, v := range m { + if _, ok := mapField[k]; !ok { + tempV := v + mapField[k] = tempV + } } } } else { diff --git a/internal/structs/structs_z_unit_test.go b/internal/structs/structs_z_unit_test.go index 4268f473a..825d095bb 100644 --- a/internal/structs/structs_z_unit_test.go +++ b/internal/structs/structs_z_unit_test.go @@ -110,7 +110,11 @@ func Test_FieldMap(t *testing.T) { Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` } var user *User - m, _ := structs.FieldMap(user, []string{"params"}, true) + m, _ := structs.FieldMap(structs.FieldMapInput{ + Pointer: user, + PriorityTagArray: []string{"params"}, + RecursiveOption: structs.RecursiveOptionEmbedded, + }) t.Assert(len(m), 3) _, ok := m["Id"] t.Assert(ok, true) @@ -130,7 +134,11 @@ func Test_FieldMap(t *testing.T) { Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` } var user *User - m, _ := structs.FieldMap(user, nil, true) + m, _ := structs.FieldMap(structs.FieldMapInput{ + Pointer: user, + PriorityTagArray: nil, + RecursiveOption: structs.RecursiveOptionEmbedded, + }) t.Assert(len(m), 3) _, ok := m["Id"] t.Assert(ok, true) diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 6e5e1a568..341afe542 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -25,7 +25,11 @@ func (v *Validator) doCheckStruct(object interface{}) Error { errorMaps = make(map[string]map[string]string) // Returning error. fieldToAliasNameMap = make(map[string]string) // Field name to alias name map. ) - fieldMap, err := structs.FieldMap(object, aliasNameTagPriority, true) + fieldMap, err := structs.FieldMap(structs.FieldMapInput{ + Pointer: object, + PriorityTagArray: aliasNameTagPriority, + RecursiveOption: structs.RecursiveOptionEmbedded, + }) if err != nil { return newErrorStr(internalObjectErrRuleName, err.Error()) }