diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index 8a49a6f25..371840a82 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -35,6 +35,7 @@ type DB interface { // The DB interface is designed not only for // relational databases but also for NoSQL databases in the future. The name // "Table" is not proper for that purpose any more. + // Deprecated, use Model instead. Table(table ...string) *Model Model(table ...string) *Model Schema(schema string) *Schema diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 2a8878a33..2b9076a3f 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -14,6 +14,7 @@ import ( "github.com/gogf/gf/internal/json" "github.com/gogf/gf/internal/utils" "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/util/gmeta" "github.com/gogf/gf/util/gutil" "reflect" "regexp" @@ -47,10 +48,16 @@ type apiMapStrAny interface { MapStrAny() map[string]interface{} } +// apiTableName is the interface for retrieving table name fro struct. +type apiTableName interface { + TableName() string +} + const ( - OrmTagForStruct = "orm" - OrmTagForUnique = "unique" - OrmTagForPrimary = "primary" + OrmTagForStruct = "orm" + OrmTagForUnique = "unique" + OrmTagForPrimary = "primary" + metaDataNameForTable = "table" ) var ( @@ -61,6 +68,26 @@ var ( structTagPriority = append([]string{OrmTagForStruct}, gconv.StructTagPriority...) ) +// getTableNameFromObject retrieves and returns the table name from struct object. +func getTableNameFromObject(object interface{}) string { + if r, ok := object.(apiTableName); ok { + // Use the interface value. + return r.TableName() + } else if table := gmeta.Get(object, metaDataNameForTable); !table.IsEmpty() { + // User meta data tag "table". + return table.String() + } else { + // Use the struct name of snake case. + if t, err := structs.StructType(object); err != nil { + panic(err) + } else { + return gstr.CaseSnakeFirstUpper( + gstr.StrEx(t.String(), "."), + ) + } + } +} + // 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. @@ -752,9 +779,16 @@ func convertMapToStruct(data map[string]interface{}, pointer interface{}) error return err } // It retrieves and returns the mapping between orm tag and the struct attribute name. - mapping := make(map[string]string) + var ( + mapping = make(map[string]string) + tagFieldName string + ) for tag, attr := range tagNameMap { - mapping[strings.Split(tag, ",")[0]] = attr + tagFieldName = strings.Split(tag, ",")[0] + if !gregex.IsMatchString(regularFieldNameRegPattern, tagFieldName) { + continue + } + mapping[tagFieldName] = attr } return gconv.Struct(data, pointer, mapping) } diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index 39cbd5213..48bea10b0 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -25,6 +25,7 @@ type Model struct { tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud". fields string // Operation fields, multiple fields joined using char ','. fieldsEx string // Excluded operation fields, multiple fields joined using char ','. + withArray []interface{} // Arguments for With feature. extraArgs []interface{} // Extra custom arguments for sql. whereHolder []*whereHolder // Condition strings for where operation. groupBy string // Used for "group by" statement. @@ -53,9 +54,13 @@ type whereHolder struct { } const ( - OPTION_OMITEMPTY = 1 + // Deprecated, use OptionOmitEmpty instead. + OPTION_OMITEMPTY = 1 + // Deprecated, use OptionAllowEmpty instead.. OPTION_ALLOWEMPTY = 2 + OptionOmitEmpty = 1 + OptionAllowEmpty = 2 linkTypeMaster = 1 linkTypeSlave = 2 whereHolderWhere = 1 @@ -63,15 +68,22 @@ const ( whereHolderOr = 3 ) -// Table creates and returns a new ORM model from given schema. -// The parameter can be more than one table names, and also alias name, like: -// 1. Table names: -// Table("user") -// Table("user u") -// Table("user, user_detail") -// Table("user u, user_detail ud") -// 2. Table name with alias: Table("user", "u") +// Table is alias of Core.Model. +// See Core.Model. +// Deprecated, use Model instead. func (c *Core) Table(table ...string) *Model { + return c.DB.Model(table...) +} + +// Model creates and returns a new ORM model from given schema. +// The parameter
can be more than one table names, and also alias name, like: +// 1. Model names: +// Model("user") +// Model("user u") +// Model("user, user_detail") +// Model("user u, user_detail ud") +// 2. Model name with alias: Model("user", "u") +func (c *Core) Model(table ...string) *Model { tables := "" if len(table) > 1 { tables = fmt.Sprintf( @@ -79,8 +91,6 @@ func (c *Core) Table(table ...string) *Model { ) } else if len(table) == 1 { tables = c.DB.QuotePrefixTableName(table[0]) - } else { - panic("table cannot be empty") } return &Model{ db: c.DB, @@ -89,31 +99,25 @@ func (c *Core) Table(table ...string) *Model { fields: "*", start: -1, offset: -1, - option: OPTION_ALLOWEMPTY, + option: OptionAllowEmpty, } } -// Model is alias of Core.Table. -// See Core.Table. -func (c *Core) Model(table ...string) *Model { - return c.DB.Table(table...) +// Table is alias of tx.Model. +// Deprecated, use Model instead. +func (tx *TX) Table(table ...string) *Model { + return tx.Model(table...) } -// Table acts like Core.Table except it operates on transaction. -// See Core.Table. -func (tx *TX) Table(table ...string) *Model { - model := tx.db.Table(table...) +// Model acts like Core.Model except it operates on transaction. +// See Core.Model. +func (tx *TX) Model(table ...string) *Model { + model := tx.db.Model(table...) model.db = tx.db model.tx = tx return model } -// Model is alias of tx.Table. -// See tx.Table. -func (tx *TX) Model(table ...string) *Model { - return tx.Table(table...) -} - // Ctx sets the context for current operation. func (m *Model) Ctx(ctx context.Context) *Model { if ctx == nil { @@ -175,7 +179,7 @@ func (m *Model) Clone() *Model { newModel = m.db.Table(m.tablesInit) } *newModel = *m - // Deep copy slice attributes. + // Shallow copy slice attributes. if n := len(m.extraArgs); n > 0 { newModel.extraArgs = make([]interface{}, n) copy(newModel.extraArgs, m.extraArgs) @@ -184,6 +188,10 @@ func (m *Model) Clone() *Model { newModel.whereHolder = make([]*whereHolder, n) copy(newModel.whereHolder, m.whereHolder) } + if n := len(m.withArray); n > 0 { + newModel.withArray = make([]interface{}, n) + copy(newModel.withArray, m.withArray) + } return newModel } diff --git a/database/gdb/gdb_model_option.go b/database/gdb/gdb_model_option.go index e41e44316..21b0acf1d 100644 --- a/database/gdb/gdb_model_option.go +++ b/database/gdb/gdb_model_option.go @@ -13,15 +13,15 @@ func (m *Model) Option(option int) *Model { return model } -// OptionOmitEmpty sets OPTION_OMITEMPTY option for the model, which automatically filers +// OptionOmitEmpty sets OptionOmitEmpty option for the model, which automatically filers // the data and where attributes for empty values. // Deprecated, use OmitEmpty instead. func (m *Model) OptionOmitEmpty() *Model { - return m.Option(OPTION_OMITEMPTY) + return m.Option(OptionOmitEmpty) } -// OmitEmpty sets OPTION_OMITEMPTY option for the model, which automatically filers +// OmitEmpty sets OptionOmitEmpty option for the model, which automatically filers // the data and where attributes for empty values. func (m *Model) OmitEmpty() *Model { - return m.Option(OPTION_OMITEMPTY) + return m.Option(OptionOmitEmpty) } diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index e3ad16442..2b93696ff 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -92,7 +92,7 @@ func (m *Model) doMappingAndFilterForInsertOrUpdateDataMap(data Map, allowOmitEm return nil, err } // Remove key-value pairs of which the value is empty. - if allowOmitEmpty && m.option&OPTION_OMITEMPTY > 0 { + if allowOmitEmpty && m.option&OptionOmitEmpty > 0 { tempMap := make(Map, len(data)) for k, v := range data { if empty.IsEmpty(v) { @@ -206,7 +206,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh case whereHolderWhere: if conditionWhere == "" { newWhere, newArgs := formatWhere( - m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0, + m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, ) if len(newWhere) > 0 { conditionWhere = newWhere @@ -218,7 +218,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh case whereHolderAnd: newWhere, newArgs := formatWhere( - m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0, + m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, ) if len(newWhere) > 0 { if len(conditionWhere) == 0 { @@ -233,7 +233,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh case whereHolderOr: newWhere, newArgs := formatWhere( - m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0, + m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, ) if len(newWhere) > 0 { if len(conditionWhere) == 0 { @@ -268,7 +268,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh // HAVING. if len(m.having) > 0 { havingStr, havingArgs := formatWhere( - m.db, m.having[0], gconv.Interfaces(m.having[1]), m.option&OPTION_OMITEMPTY > 0, + m.db, m.having[0], gconv.Interfaces(m.having[1]), m.option&OptionOmitEmpty > 0, ) if len(havingStr) > 0 { conditionExtra += " HAVING " + havingStr diff --git a/database/gdb/gdb_model_with.go b/database/gdb/gdb_model_with.go new file mode 100644 index 000000000..ec6764585 --- /dev/null +++ b/database/gdb/gdb_model_with.go @@ -0,0 +1,17 @@ +// 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 + +func (m *Model) With(structAttrPointer interface{}) *Model { + model := m.getModel() + if m.tables == "" { + m.tables = getTableNameFromObject(structAttrPointer) + return model + } + model.withArray = append(model.withArray, structAttrPointer) + return model +} diff --git a/database/gdb/gdb_type_result_scanlist.go b/database/gdb/gdb_type_result_scanlist.go index b850ec9d1..07578ffa0 100644 --- a/database/gdb/gdb_type_result_scanlist.go +++ b/database/gdb/gdb_type_result_scanlist.go @@ -226,7 +226,6 @@ func (r Result) ScanList(listPointer interface{}, bindToAttrName string, relatio ) } else { relationBindToSubAttrName = key - } } relationBindToSubAttrNameChecked = true diff --git a/database/gdb/gdb_z_mysql_association_with_test.go b/database/gdb/gdb_z_mysql_association_with_test.go index 1540b32d5..216e83942 100644 --- a/database/gdb/gdb_z_mysql_association_with_test.go +++ b/database/gdb/gdb_z_mysql_association_with_test.go @@ -6,311 +6,103 @@ package gdb_test -//func Test_Table_Relation_With(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 UserDetail struct { -// Uid int `json:"uid"` -// Address string `json:"address"` -// } -// type UserScores struct { -// Id int `json:"id"` -// Uid int `json:"uid"` -// Score int `json:"score"` -// } -// -// type User struct { -// Id int `json:"id"` -// Name string `json:"name"` -// UserDetail *UserDetail `orm:"with:uid=uid"` -// 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) -// } -// } -// // 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", gdb.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", gdb.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", gdb.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", gdb.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", gdb.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", gdb.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", gdb.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", gdb.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) -// }) -// -// // Model ScanList with pointer elements and pointer attributes. -// gtest.C(t, func(t *gtest.T) { -// var users []*Entity -// // User -// err := db.Table(tableUser). -// Where("uid", g.Slice{3, 4}). -// Order("uid asc"). -// ScanList(&users, "User") -// t.Assert(err, nil) -// // Detail -// err = db.Table(tableUserDetail). -// Where("uid", gdb.ListItemValues(users, "User", "Uid")). -// Order("uid asc"). -// ScanList(&users, "UserDetail", "User", "uid:Uid") -// gtest.Assert(err, nil) -// // Scores -// err = db.Table(tableUserScores). -// Where("uid", gdb.ListItemValues(users, "User", "Uid")). -// Order("id asc"). -// ScanList(&users, "UserScores", "User", "uid:Uid") -// 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"}) -// -// t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"}) -// t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"}) -// -// 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) -// }) -//} +import ( + "fmt" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/test/gtest" + "github.com/gogf/gf/util/gmeta" + "testing" +) + +func Test_Table_Relation_With(t *testing.T) { + var ( + tableUser = "user_with" + tableUserDetail = "user_detail_with" + tableUserScores = "user_scores_with" + ) + 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 `table:"user_detail_with"` + Uid int `json:"uid"` + Address string `json:"address"` + } + + type UserScores struct { + gmeta.Meta `table:"user_scores_with"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type User struct { + gmeta.Meta `table:"user_with"` + Id int `json:"id"` + Name string `json:"name"` + UserDetail *UserDetail `orm:"with:uid=id"` + 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) + } + } + gtest.C(t, func(t *gtest.T) { + var user *User + err := db.Model().With(&user).Where("id", 3).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 3) + }) +} diff --git a/database/gdb/gdb_z_mysql_model_test.go b/database/gdb/gdb_z_mysql_model_test.go index df40dd8d3..a1f14f511 100644 --- a/database/gdb/gdb_z_mysql_model_test.go +++ b/database/gdb/gdb_z_mysql_model_test.go @@ -2013,7 +2013,7 @@ func Test_Model_Option_Map(t *testing.T) { gtest.C(t, func(t *gtest.T) { table := createTable() defer dropTable(table) - r, err := db.Table(table).Option(gdb.OPTION_OMITEMPTY).Data(g.Map{ + r, err := db.Table(table).Option(gdb.OptionOmitEmpty).Data(g.Map{ "id": 1, "passport": 0, "password": 0, @@ -2033,7 +2033,7 @@ func Test_Model_Option_Map(t *testing.T) { gtest.C(t, func(t *gtest.T) { table := createInitTable() defer dropTable(table) - _, err := db.Table(table).Option(gdb.OPTION_OMITEMPTY).Data(g.Map{ + _, err := db.Table(table).Option(gdb.OptionOmitEmpty).Data(g.Map{ "id": 1, "passport": 0, "password": 0, @@ -2069,7 +2069,7 @@ func Test_Model_Option_Map(t *testing.T) { gtest.C(t, func(t *gtest.T) { table := createTable() defer dropTable(table) - _, err := db.Table(table).Option(gdb.OPTION_OMITEMPTY).Data(g.Map{ + _, err := db.Table(table).Option(gdb.OptionOmitEmpty).Data(g.Map{ "id": 1, "passport": 0, "password": 0, @@ -2106,7 +2106,7 @@ func Test_Model_Option_Map(t *testing.T) { n, _ := r.RowsAffected() t.Assert(n, 1) - _, err = db.Table(table).Option(gdb.OPTION_OMITEMPTY).Data(g.Map{"nickname": ""}).Where("id", 2).Update() + _, err = db.Table(table).Option(gdb.OptionOmitEmpty).Data(g.Map{"nickname": ""}).Where("id", 2).Update() t.AssertNE(err, nil) r, err = db.Table(table).OmitEmpty().Data(g.Map{"nickname": "", "password": "123"}).Where("id", 3).Update() diff --git a/frame/g/g_object.go b/frame/g/g_object.go index eee9bb8a4..6cfacac4b 100644 --- a/frame/g/g_object.go +++ b/frame/g/g_object.go @@ -92,15 +92,17 @@ func DB(name ...string) gdb.DB { } // Table is alias of Model. -func Table(tables string, db ...string) *gdb.Model { - return DB(db...).Table(tables) +// The database component is designed not only for +// relational databases but also for NoSQL databases in the future. The name +// "Table" is not proper for that purpose any more. +// Deprecated, use Model instead. +func Table(tables ...string) *gdb.Model { + return DB().Table(tables...) } -// Model creates and returns a model from specified database or default database configuration. -// The optional parameter specifies the configuration group name of the database, -// which is "default" in default. -func Model(tables string, db ...string) *gdb.Model { - return DB(db...).Model(tables) +// Model creates and returns a model based on configuration of default database group. +func Model(tables ...string) *gdb.Model { + return DB().Model(tables...) } // Redis returns an instance of redis client with specified configuration group name. diff --git a/internal/structs/structs.go b/internal/structs/structs.go index 2fa28f523..5ff9190db 100644 --- a/internal/structs/structs.go +++ b/internal/structs/structs.go @@ -13,6 +13,11 @@ import ( "reflect" ) +// Type wraps reflect.Type for additional features. +type Type struct { + reflect.Type +} + // Field contains information of a struct field . type Field struct { value reflect.Value diff --git a/internal/structs/structs_type.go b/internal/structs/structs_type.go new file mode 100644 index 000000000..cee57b2f7 --- /dev/null +++ b/internal/structs/structs_type.go @@ -0,0 +1,55 @@ +// 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 structs + +import ( + "github.com/gogf/gf/errors/gerror" + "reflect" +) + +// StructType retrieves and returns the Type of specified struct/*struct. +func StructType(structOrPointer interface{}) (*Type, error) { + var ( + reflectValue reflect.Value + reflectKind reflect.Kind + reflectType reflect.Type + ) + if rv, ok := structOrPointer.(reflect.Value); ok { + reflectValue = rv + } else { + reflectValue = reflect.ValueOf(structOrPointer) + } + reflectKind = reflectValue.Kind() + for reflectKind == reflect.Ptr { + if !reflectValue.IsValid() || reflectValue.IsNil() { + // If pointer is type of *struct and nil, then automatically create a temporary struct. + reflectValue = reflect.New(reflectValue.Type().Elem()).Elem() + reflectKind = reflectValue.Kind() + } else { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + } + for reflectKind == reflect.Ptr { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + if reflectKind != reflect.Struct { + return nil, gerror.Newf( + `invalid parameter kind "%s", kind of "struct" is required`, + reflectKind, + ) + } + reflectType = reflectValue.Type() + return &Type{ + Type: reflectType, + }, nil +} + +func (t *Type) Signature() string { + return t.PkgPath() + "/" + t.String() +} diff --git a/internal/structs/structs_z_unit_test.go b/internal/structs/structs_z_unit_test.go index 5124c6cce..f694aecdf 100644 --- a/internal/structs/structs_z_unit_test.go +++ b/internal/structs/structs_z_unit_test.go @@ -124,3 +124,39 @@ func Test_MapField(t *testing.T) { t.Assert(ok, true) }) } + +func Test_StructType(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type B struct { + Name string + } + type A struct { + B + } + r, err := structs.StructType(new(A)) + t.AssertNil(err) + t.Assert(r.Signature(), `github.com/gogf/gf/internal/structs_test/structs_test.A`) + }) + gtest.C(t, func(t *gtest.T) { + type B struct { + Name string + } + type A struct { + B + } + r, err := structs.StructType(new(A).B) + t.AssertNil(err) + t.Assert(r.Signature(), `github.com/gogf/gf/internal/structs_test/structs_test.B`) + }) + // Error. + gtest.C(t, func(t *gtest.T) { + type B struct { + Name string + } + type A struct { + *B + } + _, err := structs.StructType(new(A).B) + t.AssertNE(err, nil) + }) +} diff --git a/text/gstr/gstr.go b/text/gstr/gstr.go index aa5fe0ca8..2758b6091 100644 --- a/text/gstr/gstr.go +++ b/text/gstr/gstr.go @@ -472,6 +472,15 @@ func Str(haystack string, needle string) string { return haystack[idx+len([]byte(needle))-1:] } +// StrEx returns part of string starting from and excluding +// the first occurrence of to the end of . +func StrEx(haystack string, needle string) string { + if s := Str(haystack, needle); s != "" { + return s[1:] + } + return "" +} + // Shuffle randomly shuffles a string. // It considers parameter as unicode string. func Shuffle(str string) string { diff --git a/text/gstr/gstr_z_unit_basic_test.go b/text/gstr/gstr_z_unit_basic_test.go index 7f04d64d1..76cb2eadc 100644 --- a/text/gstr/gstr_z_unit_basic_test.go +++ b/text/gstr/gstr_z_unit_basic_test.go @@ -295,6 +295,14 @@ func Test_Str(t *testing.T) { }) } +func Test_StrEx(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(gstr.StrEx("name@example.com", "@"), "example.com") + t.Assert(gstr.StrEx("name@example.com", ""), "") + t.Assert(gstr.StrEx("name@example.com", "z"), "") + }) +} + func Test_Shuffle(t *testing.T) { gtest.C(t, func(t *gtest.T) { t.Assert(len(gstr.Shuffle("123456")), 6) diff --git a/util/gmeta/gmeta.go b/util/gmeta/gmeta.go new file mode 100644 index 000000000..2a2c357a3 --- /dev/null +++ b/util/gmeta/gmeta.go @@ -0,0 +1,94 @@ +// 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 gmeta provides embedded meta data feature for struct. +package gmeta + +import ( + "github.com/gogf/gf/container/gmap" + "github.com/gogf/gf/container/gvar" + "github.com/gogf/gf/internal/structs" + "strconv" +) + +// Meta is used as an embedded attribute for struct to enabled meta data feature. +type Meta struct{} + +const ( + metaAttributeName = "Meta" +) + +var ( + metaDataCacheMap = gmap.NewStrAnyMap(true) +) + +// Data retrieves and returns all meta data from `object`. +// It automatically parses the tag string from "Mata" attribute as its meta data. +func Data(object interface{}) map[string]interface{} { + reflectType, err := structs.StructType(object) + if err != nil { + panic(err) + } + return metaDataCacheMap.GetOrSetFuncLock(reflectType.Signature(), func() interface{} { + if field, ok := reflectType.FieldByName(metaAttributeName); ok { + var ( + key string + tag = field.Tag + data = make(map[string]interface{}) + ) + for tag != "" { + // Skip leading space. + i := 0 + for i < len(tag) && tag[i] == ' ' { + i++ + } + tag = tag[i:] + if tag == "" { + break + } + // Scan to colon. A space, a quote or a control character is a syntax error. + // Strictly speaking, control chars include the range [0x7f, 0x9f], not just + // [0x00, 0x1f], but in practice, we ignore the multi-byte control characters + // as it is simpler to inspect the tag's bytes than the tag's runes. + i = 0 + for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f { + i++ + } + if i == 0 || i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' { + break + } + key = string(tag[:i]) + tag = tag[i+1:] + + // Scan quoted string to find value. + i = 1 + for i < len(tag) && tag[i] != '"' { + if tag[i] == '\\' { + i++ + } + i++ + } + if i >= len(tag) { + break + } + quotedValue := string(tag[:i+1]) + tag = tag[i+1:] + value, err := strconv.Unquote(quotedValue) + if err != nil { + panic(err) + } + data[key] = value + } + return data + } + return map[string]interface{}{} + }).(map[string]interface{}) +} + +// Get retrieves and returns specified meta data by `key` from `object`. +func Get(object interface{}, key string) *gvar.Var { + return gvar.New(Data(object)[key]) +} diff --git a/util/gmeta/gmeta_test.go b/util/gmeta/gmeta_test.go new file mode 100644 index 000000000..2186d5414 --- /dev/null +++ b/util/gmeta/gmeta_test.go @@ -0,0 +1,78 @@ +// 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 gmeta_test + +import ( + "github.com/gogf/gf/internal/json" + "github.com/gogf/gf/util/gmeta" + "testing" + + "github.com/gogf/gf/test/gtest" +) + +type A struct { + gmeta.Meta `tag:"123" orm:"456"` + Id int + Name string +} + +var ( + a1 A + a2 *A +) + +func Benchmark_Data_Struct(b *testing.B) { + for i := 0; i < b.N; i++ { + gmeta.Data(a1) + } +} + +func Benchmark_Data_Pointer1(b *testing.B) { + for i := 0; i < b.N; i++ { + gmeta.Data(a2) + } +} + +func Benchmark_Data_Pointer2(b *testing.B) { + for i := 0; i < b.N; i++ { + gmeta.Data(&a2) + } +} + +func Benchmark_Data_Get_Struct(b *testing.B) { + for i := 0; i < b.N; i++ { + gmeta.Get(a1, "tag") + } +} + +func Benchmark_Data_Get_Pointer1(b *testing.B) { + for i := 0; i < b.N; i++ { + gmeta.Get(a2, "tag") + } +} + +func Benchmark_Data_Get_Pointer2(b *testing.B) { + for i := 0; i < b.N; i++ { + gmeta.Get(&a2, "tag") + } +} + +func TestMeta_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + a := &A{ + Id: 100, + Name: "john", + } + t.Assert(len(gmeta.Data(a)), 2) + t.Assert(gmeta.Get(a, "tag").String(), "123") + t.Assert(gmeta.Get(a, "orm").String(), "456") + + b, err := json.Marshal(a) + t.AssertNil(err) + t.Assert(b, `{"Id":100,"Name":"john"}`) + }) +}