diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index c21a91809..6272caa74 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -11,6 +11,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/internal/intlog" "time" @@ -18,7 +19,6 @@ import ( "github.com/gogf/gf/container/gmap" "github.com/gogf/gf/container/gtype" - "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/os/gcache" "github.com/gogf/gf/util/grand" ) @@ -89,6 +89,7 @@ type DB interface { SetSchema(schema string) GetSchema() string GetPrefix() string + GetGroup() string SetDryRun(dryrun bool) GetDryRun() bool SetLogger(logger *glog.Logger) @@ -169,21 +170,23 @@ type Link interface { Prepare(sql string) (*sql.Stmt, error) } -// Value is the field value type. -type Value = *gvar.Var +type ( + // Value is the field value type. + Value = *gvar.Var -// Record is the row record of the table. -type Record map[string]Value + // Record is the row record of the table. + Record map[string]Value -// Result is the row record array. -type Result []Record + // Result is the row record array. + Result []Record -// Map is alias of map[string]interface{}, -// which is the most common usage map type. -type Map = map[string]interface{} + // Map is alias of map[string]interface{}, + // which is the most common usage map type. + Map = map[string]interface{} -// List is type of map array. -type List = []Map + // List is type of map array. + List = []Map +) const ( gINSERT_OPTION_DEFAULT = 0 diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index 31e064230..ffa79bfac 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -157,7 +157,7 @@ func (c *Core) GetAll(sql string, args ...interface{}) (Result, error) { return c.DB.DoGetAll(nil, sql, args...) } -// doGetAll queries and returns data records from database. +// DoGetAll queries and returns data records from database. func (c *Core) DoGetAll(link Link, sql string, args ...interface{}) (result Result, err error) { if link == nil { link, err = c.DB.Slave() @@ -379,13 +379,15 @@ func (c *Core) Save(table string, data interface{}, batch ...int) (sql.Result, e // 2: save: if there's unique/primary key in the data, it updates it or else inserts a new one; // 3: ignore: if there's unique/primary key in the data, it ignores the inserting; func (c *Core) DoInsert(link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) { - var fields []string - var values []string - var params []interface{} - var dataMap Map table = c.DB.QuotePrefixTableName(table) - reflectValue := reflect.ValueOf(data) - reflectKind := reflectValue.Kind() + var ( + fields []string + values []string + params []interface{} + dataMap Map + reflectValue = reflect.ValueOf(data) + reflectKind = reflectValue.Kind() + ) if reflectKind == reflect.Ptr { reflectValue = reflectValue.Elem() reflectKind = reflectValue.Kind() @@ -401,16 +403,23 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b if len(dataMap) == 0 { return nil, errors.New("data cannot be empty") } - charL, charR := c.DB.GetChars() + var ( + charL, charR = c.DB.GetChars() + operation = GetInsertOperationByOption(option) + updateStr = "" + ) for k, v := range dataMap { fields = append(fields, charL+k+charR) values = append(values, "?") params = append(params, v) } - operation := GetInsertOperationByOption(option) - updateStr := "" if option == gINSERT_OPTION_SAVE { for k, _ := range dataMap { + // If it's SAVE operation, + // do not automatically update the creating time. + if k == gSOFT_FIELD_NAME_CREATE { + continue + } if len(updateStr) > 0 { updateStr += "," } @@ -462,12 +471,15 @@ func (c *Core) BatchSave(table string, list interface{}, batch ...int) (sql.Resu return c.DB.DoBatchInsert(nil, table, list, gINSERT_OPTION_SAVE, batch...) } -// doBatchInsert batch inserts/replaces/saves data. +// DoBatchInsert batch inserts/replaces/saves data. func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) { - var keys, values []string - var params []interface{} table = c.DB.QuotePrefixTableName(table) - listMap := (List)(nil) + var ( + keys []string + values []string + params []interface{} + listMap List + ) switch v := list.(type) { case Result: listMap = v.List() @@ -478,8 +490,10 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i case Map: listMap = List{v} default: - rv := reflect.ValueOf(list) - kind := rv.Kind() + var ( + rv = reflect.ValueOf(list) + kind = rv.Kind() + ) if kind == reflect.Ptr { rv = rv.Elem() kind = rv.Kind() @@ -512,15 +526,21 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i holders = append(holders, "?") } // Prepare the batch result pointer. - batchResult := new(SqlResult) - charL, charR := c.DB.GetChars() - keysStr := charL + strings.Join(keys, charR+","+charL) + charR - valueHolderStr := "(" + strings.Join(holders, ",") + ")" - - operation := GetInsertOperationByOption(option) - updateStr := "" + var ( + charL, charR = c.DB.GetChars() + batchResult = new(SqlResult) + keysStr = charL + strings.Join(keys, charR+","+charL) + charR + valueHolderStr = "(" + strings.Join(holders, ",") + ")" + operation = GetInsertOperationByOption(option) + updateStr = "" + ) if option == gINSERT_OPTION_SAVE { for _, k := range keys { + // If it's SAVE operation, + // do not automatically update the creating time. + if k == gSOFT_FIELD_NAME_CREATE { + continue + } if len(updateStr) > 0 { updateStr += "," } @@ -599,18 +619,25 @@ func (c *Core) Update(table string, data interface{}, condition interface{}, arg // Also see Update. func (c *Core) DoUpdate(link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) { table = c.DB.QuotePrefixTableName(table) - updates := "" - rv := reflect.ValueOf(data) - kind := rv.Kind() + var ( + rv = reflect.ValueOf(data) + kind = rv.Kind() + ) if kind == reflect.Ptr { rv = rv.Elem() kind = rv.Kind() } - params := []interface{}(nil) + var ( + params []interface{} + updates = "" + ) switch kind { case reflect.Map, reflect.Struct: - var fields []string - for k, v := range DataToMapDeep(data) { + var ( + fields []string + dataMap = DataToMapDeep(data) + ) + for k, v := range dataMap { fields = append(fields, c.DB.QuoteWord(k)+"=?") params = append(params, v) } @@ -656,7 +683,7 @@ func (c *Core) Delete(table string, condition interface{}, args ...interface{}) return c.DB.DoDelete(nil, table, newWhere, newArgs...) } -// doDelete does "DELETE FROM ... " statement for the table. +// DoDelete does "DELETE FROM ... " statement for the table. // Also see Delete. func (c *Core) DoDelete(link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) { if link == nil { diff --git a/database/gdb/gdb_core_config.go b/database/gdb/gdb_core_config.go index 3c78665a6..9f3b2e52d 100644 --- a/database/gdb/gdb_core_config.go +++ b/database/gdb/gdb_core_config.go @@ -174,6 +174,11 @@ func (c *Core) GetPrefix() string { return c.prefix } +// GetGroup returns the group string configured. +func (c *Core) GetGroup() string { + return c.group +} + // SetDryRun enables/disables the DryRun feature. func (c *Core) SetDryRun(dryrun bool) { c.dryrun.Set(dryrun) diff --git a/database/gdb/gdb_driver_mssql.go b/database/gdb/gdb_driver_mssql.go index ead5aaca1..26aca8b98 100644 --- a/database/gdb/gdb_driver_mssql.go +++ b/database/gdb/gdb_driver_mssql.go @@ -13,6 +13,7 @@ package gdb import ( "database/sql" + "errors" "fmt" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gstr" @@ -187,9 +188,10 @@ func (d *DriverMssql) Tables(schema ...string) (tables []string, err error) { // TableFields retrieves and returns the fields information of specified table of current schema. func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { - table = gstr.Trim(table) + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { - panic("function TableFields supports only single table operations") + return nil, errors.New("function TableFields supports only single table operations") } checkSchema := d.DB.GetSchema() if len(schema) > 0 && schema[0] != "" { diff --git a/database/gdb/gdb_driver_mysql.go b/database/gdb/gdb_driver_mysql.go index 02f81660b..2d0dea44a 100644 --- a/database/gdb/gdb_driver_mysql.go +++ b/database/gdb/gdb_driver_mysql.go @@ -8,6 +8,7 @@ package gdb import ( "database/sql" + "errors" "fmt" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gregex" @@ -89,9 +90,10 @@ func (d *DriverMysql) Tables(schema ...string) (tables []string, err error) { // // It's using cache feature to enhance the performance, which is never expired util the process restarts. func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { - table = gstr.Trim(table) + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { - panic("function TableFields supports only single table operations") + return nil, errors.New("function TableFields supports only single table operations") } checkSchema := d.schema.Val() if len(schema) > 0 && schema[0] != "" { diff --git a/database/gdb/gdb_driver_oracle.go b/database/gdb/gdb_driver_oracle.go index d6ccd40fe..7adca6dfc 100644 --- a/database/gdb/gdb_driver_oracle.go +++ b/database/gdb/gdb_driver_oracle.go @@ -148,9 +148,10 @@ func (d *DriverOracle) Tables(schema ...string) (tables []string, err error) { // TableFields retrieves and returns the fields information of specified table of current schema. func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { - table = gstr.Trim(table) + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { - panic("function TableFields supports only single table operations") + return nil, errors.New("function TableFields supports only single table operations") } checkSchema := d.DB.GetSchema() if len(schema) > 0 && schema[0] != "" { diff --git a/database/gdb/gdb_driver_pgsql.go b/database/gdb/gdb_driver_pgsql.go index 1e0bc5a88..9bff38467 100644 --- a/database/gdb/gdb_driver_pgsql.go +++ b/database/gdb/gdb_driver_pgsql.go @@ -13,6 +13,7 @@ package gdb import ( "database/sql" + "errors" "fmt" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gstr" @@ -78,7 +79,6 @@ func (d *DriverPgsql) Tables(schema ...string) (tables []string, err error) { if err != nil { return nil, err } - query := "SELECT TABLENAME FROM PG_TABLES WHERE SCHEMANAME = 'public' ORDER BY TABLENAME" if len(schema) > 0 && schema[0] != "" { query = fmt.Sprintf("SELECT TABLENAME FROM PG_TABLES WHERE SCHEMANAME = '%s' ORDER BY TABLENAME", schema[0]) @@ -97,9 +97,10 @@ func (d *DriverPgsql) Tables(schema ...string) (tables []string, err error) { // TableFields retrieves and returns the fields information of specified table of current schema. func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { - table = gstr.Trim(table) + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { - panic("function TableFields supports only single table operations") + return nil, errors.New("function TableFields supports only single table operations") } table, _ = gregex.ReplaceString("\"", "", table) checkSchema := d.DB.GetSchema() diff --git a/database/gdb/gdb_driver_sqlite.go b/database/gdb/gdb_driver_sqlite.go index c433fcba3..3f9eea3b8 100644 --- a/database/gdb/gdb_driver_sqlite.go +++ b/database/gdb/gdb_driver_sqlite.go @@ -12,6 +12,7 @@ package gdb import ( "database/sql" + "errors" "fmt" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/os/gfile" @@ -88,11 +89,11 @@ func (d *DriverSqlite) Tables(schema ...string) (tables []string, err error) { // TableFields retrieves and returns the fields information of specified table of current schema. func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { - table = gstr.Trim(table) + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { - panic("function TableFields supports only single table operations") + return nil, errors.New("function TableFields supports only single table operations") } - checkSchema := d.DB.GetSchema() if len(schema) > 0 && schema[0] != "" { checkSchema = schema[0] diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index 7752ac259..98b71493a 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -37,6 +37,7 @@ type Model struct { cacheEnabled bool // Enable sql result cache feature. cacheDuration time.Duration // Cache TTL duration. cacheName string // Cache name for custom operation. + force bool // Force select/delete without soft operation features. safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. } diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index e0638a8f7..190a3f00f 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -8,8 +8,21 @@ package gdb import ( "database/sql" + "fmt" + "github.com/gogf/gf/os/gtime" ) +// Force enables/disables the soft deleting feature. +func (m *Model) Force(force ...bool) *Model { + model := m.getModel() + if len(force) > 0 { + model.force = force[0] + } else { + model.force = true + } + return model +} + // Delete does "DELETE FROM ... " statement for the model. // The optional parameter is the same as the parameter of Model.Where function, // see Model.Where. @@ -22,6 +35,19 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { m.checkAndRemoveCache() } }() - condition, conditionArgs := m.formatCondition(false) - return m.db.DoDelete(m.getLink(true), m.tables, condition, conditionArgs...) + var ( + fieldNameDelete = m.getSoftFieldNameDelete() + conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false) + ) + // Soft deleting. + if !m.force && fieldNameDelete != "" { + return m.db.DoUpdate( + m.getLink(true), + m.tables, + fmt.Sprintf(`%s='%s'`, m.db.QuoteWord(fieldNameDelete), gtime.Now().String()), + conditionWhere+conditionExtra, + conditionArgs..., + ) + } + return m.db.DoDelete(m.getLink(true), m.tables, conditionWhere+conditionExtra, conditionArgs...) } diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index 255e4529a..f78708770 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -9,8 +9,10 @@ package gdb import ( "database/sql" "errors" + "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" "reflect" ) @@ -94,6 +96,9 @@ func (m *Model) Data(data ...interface{}) *Model { // The optional parameter is the same as the parameter of Model.Data function, // see Model.Data. func (m *Model) Insert(data ...interface{}) (result sql.Result, err error) { + if len(data) > 0 { + return m.Data(data...).Insert() + } return m.doInsertWithOption(gINSERT_OPTION_DEFAULT, data...) } @@ -101,45 +106,10 @@ func (m *Model) Insert(data ...interface{}) (result sql.Result, err error) { // The optional parameter is the same as the parameter of Model.Data function, // see Model.Data. func (m *Model) InsertIgnore(data ...interface{}) (result sql.Result, err error) { - return m.doInsertWithOption(gINSERT_OPTION_IGNORE, data...) -} - -// doInsertWithOption inserts data with option parameter. -func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.Result, err error) { if len(data) > 0 { return m.Data(data...).Insert() } - defer func() { - if err == nil { - m.checkAndRemoveCache() - } - }() - if m.data == nil { - return nil, errors.New("inserting into table with empty data") - } - if list, ok := m.data.(List); ok { - // Batch insert. - batch := 10 - if m.batch > 0 { - batch = m.batch - } - return m.db.DoBatchInsert( - m.getLink(true), - m.tables, - m.filterDataForInsertOrUpdate(list), - option, - batch, - ) - } else if data, ok := m.data.(Map); ok { - // Single insert. - return m.db.DoInsert( - m.getLink(true), - m.tables, - m.filterDataForInsertOrUpdate(data), - option, - ) - } - return nil, errors.New("inserting into table with invalid data type") + return m.doInsertWithOption(gINSERT_OPTION_IGNORE, data...) } // Replace does "REPLACE INTO ..." statement for the model. @@ -149,37 +119,7 @@ func (m *Model) Replace(data ...interface{}) (result sql.Result, err error) { if len(data) > 0 { return m.Data(data...).Replace() } - defer func() { - if err == nil { - m.checkAndRemoveCache() - } - }() - if m.data == nil { - return nil, errors.New("replacing into table with empty data") - } - if list, ok := m.data.(List); ok { - // Batch replace. - batch := 10 - if m.batch > 0 { - batch = m.batch - } - return m.db.DoBatchInsert( - m.getLink(true), - m.tables, - m.filterDataForInsertOrUpdate(list), - gINSERT_OPTION_REPLACE, - batch, - ) - } else if data, ok := m.data.(Map); ok { - // Single insert. - return m.db.DoInsert( - m.getLink(true), - m.tables, - m.filterDataForInsertOrUpdate(data), - gINSERT_OPTION_REPLACE, - ) - } - return nil, errors.New("replacing into table with invalid data type") + return m.doInsertWithOption(gINSERT_OPTION_REPLACE, data...) } // Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the model. @@ -192,35 +132,67 @@ func (m *Model) Save(data ...interface{}) (result sql.Result, err error) { if len(data) > 0 { return m.Data(data...).Save() } + return m.doInsertWithOption(gINSERT_OPTION_SAVE, data...) +} + +// doInsertWithOption inserts data with option parameter. +func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.Result, err error) { defer func() { if err == nil { m.checkAndRemoveCache() } }() if m.data == nil { - return nil, errors.New("saving into table with empty data") + return nil, errors.New("inserting into table with empty data") } + var ( + nowString = gtime.Now().String() + fieldNameCreate = m.getSoftFieldNameCreate() + fieldNameUpdate = m.getSoftFieldNameUpdate() + ) + // Batch operation. if list, ok := m.data.(List); ok { - // Batch save. batch := gDEFAULT_BATCH_NUM if m.batch > 0 { batch = m.batch } + // Automatic handling for creating/updating time. + if !m.force && (fieldNameCreate != "" || fieldNameUpdate != "") { + for k, v := range list { + if fieldNameCreate != "" && !gutil.MapContainsPossibleKey(v, fieldNameCreate) { + v[fieldNameCreate] = nowString + } + if fieldNameUpdate != "" && !gutil.MapContainsPossibleKey(v, fieldNameUpdate) { + v[fieldNameUpdate] = nowString + } + list[k] = v + } + } return m.db.DoBatchInsert( m.getLink(true), m.tables, m.filterDataForInsertOrUpdate(list), - gINSERT_OPTION_SAVE, + option, batch, ) - } else if data, ok := m.data.(Map); ok { - // Single save. + } + // Single operation. + if data, ok := m.data.(Map); ok { + // Automatic handling for creating/updating time. + if !m.force && (fieldNameCreate != "" || fieldNameUpdate != "") { + if fieldNameCreate != "" && !gutil.MapContainsPossibleKey(data, fieldNameCreate) { + data[fieldNameCreate] = nowString + } + if fieldNameUpdate != "" && !gutil.MapContainsPossibleKey(data, fieldNameUpdate) { + data[fieldNameUpdate] = nowString + } + } return m.db.DoInsert( m.getLink(true), m.tables, m.filterDataForInsertOrUpdate(data), - gINSERT_OPTION_SAVE, + option, ) } - return nil, errors.New("saving into table with invalid data type") + return nil, errors.New("inserting into table with invalid data type") } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index c3eefcb01..d873bc12c 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -30,9 +30,18 @@ func (m *Model) All(where ...interface{}) (Result, error) { if len(where) > 0 { return m.Where(where[0], where[1:]...).All() } - condition, conditionArgs := m.formatCondition(false) + var ( + fieldNameDelete = m.getSoftFieldNameDelete() + conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false) + ) + if !m.force && fieldNameDelete != "" { + if conditionWhere != "" { + conditionWhere += " AND" + } + conditionWhere += fmt.Sprintf(` %s IS NULL`, m.db.QuoteWord(fieldNameDelete)) + } return m.getAll( - fmt.Sprintf("SELECT %s FROM %s%s", m.fields, m.tables, condition), + fmt.Sprintf("SELECT %s FROM %s%s", m.fields, m.tables, conditionWhere+conditionExtra), conditionArgs..., ) } @@ -73,8 +82,7 @@ func (m *Model) One(where ...interface{}) (Record, error) { if len(where) > 0 { return m.Where(where[0], where[1:]...).One() } - condition, conditionArgs := m.formatCondition(true) - all, err := m.getAll(fmt.Sprintf("SELECT %s FROM %s%s", m.fields, m.tables, condition), conditionArgs...) + all, err := m.All() if err != nil { return nil, err } @@ -236,8 +244,8 @@ func (m *Model) Count(where ...interface{}) (int, error) { if m.fields != "" && m.fields != "*" { countFields = fmt.Sprintf(`COUNT(%s)`, m.fields) } - condition, conditionArgs := m.formatCondition(false) - s := fmt.Sprintf("SELECT %s FROM %s %s", countFields, m.tables, condition) + conditionWhere, conditionExtra, conditionArgs := m.formatCondition(false) + s := fmt.Sprintf("SELECT %s FROM %s %s", countFields, m.tables, conditionWhere+conditionExtra) if len(m.groupBy) > 0 { s = fmt.Sprintf("SELECT COUNT(1) FROM (%s) count_alias", s) } diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go new file mode 100644 index 000000000..c3351a328 --- /dev/null +++ b/database/gdb/gdb_model_time.go @@ -0,0 +1,57 @@ +// 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 + +import ( + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" +) + +const ( + gSOFT_FIELD_NAME_CREATE = "create_at" + gSOFT_FIELD_NAME_UPDATE = "update_at" + gSOFT_FIELD_NAME_DELETE = "delete_at" +) + +// getSoftFieldNameCreate checks and returns the field name for record creating time. +// If there's no field name for storing creating time, it returns an empty string. +// It checks the key with or without cases or chars '-'/'_'/'.'/' '. +func (m *Model) getSoftFieldNameCreate() (field string) { + fieldsMap, _ := m.db.TableFields(m.tables) + if len(fieldsMap) > 0 { + field, _ = gutil.MapPossibleItemByKey( + gconv.Map(fieldsMap), gSOFT_FIELD_NAME_CREATE, + ) + } + return +} + +// getSoftFieldNameUpdate checks and returns the field name for record updating time. +// If there's no field name for storing updating time, it returns an empty string. +// It checks the key with or without cases or chars '-'/'_'/'.'/' '. +func (m *Model) getSoftFieldNameUpdate() (field string) { + fieldsMap, _ := m.db.TableFields(m.tables) + if len(fieldsMap) > 0 { + field, _ = gutil.MapPossibleItemByKey( + gconv.Map(fieldsMap), gSOFT_FIELD_NAME_UPDATE, + ) + } + return +} + +// getSoftFieldNameDelete checks and returns the field name for record deleting time. +// If there's no field name for storing deleting time, it returns an empty string. +// It checks the key with or without cases or chars '-'/'_'/'.'/' '. +func (m *Model) getSoftFieldNameDelete() (field string) { + fieldsMap, _ := m.db.TableFields(m.tables) + if len(fieldsMap) > 0 { + field, _ = gutil.MapPossibleItemByKey( + gconv.Map(fieldsMap), gSOFT_FIELD_NAME_DELETE, + ) + } + return +} diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index a8cdf7019..7f9ce5240 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -9,6 +9,12 @@ package gdb import ( "database/sql" "errors" + "fmt" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" + "reflect" ) // Update does "UPDATE ... " statement for the model. @@ -34,12 +40,41 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro if m.data == nil { return nil, errors.New("updating table with empty data") } - condition, conditionArgs := m.formatCondition(false) + var ( + updateData = m.data + fieldNameUpdate = m.getSoftFieldNameUpdate() + conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false) + ) + // Automatically update the record updating time. + if !m.force && fieldNameUpdate != "" { + var ( + refValue = reflect.ValueOf(m.data) + refKind = refValue.Kind() + ) + if refKind == reflect.Ptr { + refValue = refValue.Elem() + refKind = refValue.Kind() + } + switch refKind { + case reflect.Map, reflect.Struct: + dataMap := DataToMapDeep(m.data) + if fieldNameUpdate != "" && !gutil.MapContainsPossibleKey(dataMap, fieldNameUpdate) { + dataMap[fieldNameUpdate] = gtime.Now().String() + } + updateData = dataMap + default: + updates := gconv.String(m.data) + if fieldNameUpdate != "" && !gstr.Contains(updates, fieldNameUpdate) { + updates += fmt.Sprintf(`,%s='%s'`, fieldNameUpdate, gtime.Now().String()) + } + updateData = updates + } + } return m.db.DoUpdate( m.getLink(true), m.tables, - m.filterDataForInsertOrUpdate(m.data), - condition, + m.filterDataForInsertOrUpdate(updateData), + conditionWhere+conditionExtra, m.mergeArguments(conditionArgs)..., ) } diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index 7acc7063b..71398af80 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -28,15 +28,19 @@ func (m *Model) getModel() *Model { // filterDataForInsertOrUpdate does filter feature with data for inserting/updating operations. // Note that, it does not filter list item, which is also type of map, for "omit empty" feature. func (m *Model) filterDataForInsertOrUpdate(data interface{}) interface{} { - if list, ok := m.data.(List); ok { - for k, item := range list { - list[k] = m.doFilterDataMapForInsertOrUpdate(item, false) + switch value := data.(type) { + case List: + for k, item := range value { + value[k] = m.doFilterDataMapForInsertOrUpdate(item, false) } - return list - } else if item, ok := m.data.(Map); ok { - return m.doFilterDataMapForInsertOrUpdate(item, true) + return value + + case Map: + return m.doFilterDataMapForInsertOrUpdate(value, true) + + default: + return data } - return data } // doFilterDataMapForInsertOrUpdate does the filter features for map. @@ -144,16 +148,15 @@ func (m *Model) checkAndRemoveCache() { // Note that this function does not change any attribute value of the . // // The parameter specifies whether limits querying only one record if m.limit is not set. -func (m *Model) formatCondition(limit bool) (condition string, conditionArgs []interface{}) { - var where string +func (m *Model) formatCondition(limit bool) (conditionWhere string, conditionExtra string, conditionArgs []interface{}) { if len(m.whereHolder) > 0 { for _, v := range m.whereHolder { switch v.operator { case gWHERE_HOLDER_WHERE: - if where == "" { + if conditionWhere == "" { newWhere, newArgs := formatWhere(m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0) if len(newWhere) > 0 { - where = newWhere + conditionWhere = newWhere conditionArgs = newArgs } continue @@ -163,10 +166,10 @@ func (m *Model) formatCondition(limit bool) (condition string, conditionArgs []i case gWHERE_HOLDER_AND: newWhere, newArgs := formatWhere(m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0) if len(newWhere) > 0 { - if where[0] == '(' { - where = fmt.Sprintf(`%s AND (%s)`, where, newWhere) + if conditionWhere[0] == '(' { + conditionWhere = fmt.Sprintf(`%s AND (%s)`, conditionWhere, newWhere) } else { - where = fmt.Sprintf(`(%s) AND (%s)`, where, newWhere) + conditionWhere = fmt.Sprintf(`(%s) AND (%s)`, conditionWhere, newWhere) } conditionArgs = append(conditionArgs, newArgs...) } @@ -174,39 +177,39 @@ func (m *Model) formatCondition(limit bool) (condition string, conditionArgs []i case gWHERE_HOLDER_OR: newWhere, newArgs := formatWhere(m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0) if len(newWhere) > 0 { - if where[0] == '(' { - where = fmt.Sprintf(`%s OR (%s)`, where, newWhere) + if conditionWhere[0] == '(' { + conditionWhere = fmt.Sprintf(`%s OR (%s)`, conditionWhere, newWhere) } else { - where = fmt.Sprintf(`(%s) OR (%s)`, where, newWhere) + conditionWhere = fmt.Sprintf(`(%s) OR (%s)`, conditionWhere, newWhere) } conditionArgs = append(conditionArgs, newArgs...) } } } } - if where != "" { - condition += " WHERE " + where + if conditionWhere != "" { + conditionWhere = " WHERE " + conditionWhere } if m.groupBy != "" { - condition += " GROUP BY " + m.groupBy + conditionExtra += " GROUP BY " + m.groupBy } if m.orderBy != "" { - condition += " ORDER BY " + m.orderBy + conditionExtra += " ORDER BY " + m.orderBy } if m.limit != 0 { if m.start >= 0 { - condition += fmt.Sprintf(" LIMIT %d,%d", m.start, m.limit) + conditionExtra += fmt.Sprintf(" LIMIT %d,%d", m.start, m.limit) } else { - condition += fmt.Sprintf(" LIMIT %d", m.limit) + conditionExtra += fmt.Sprintf(" LIMIT %d", m.limit) } } else if limit { - condition += " LIMIT 1" + conditionExtra += " LIMIT 1" } if m.offset >= 0 { - condition += fmt.Sprintf(" OFFSET %d", m.offset) + conditionExtra += fmt.Sprintf(" OFFSET %d", m.offset) } if m.lockInfo != "" { - condition += " " + m.lockInfo + conditionExtra += " " + m.lockInfo } return } diff --git a/database/gdb/gdb_schema.go b/database/gdb/gdb_schema.go index 875869a55..6671fcca8 100644 --- a/database/gdb/gdb_schema.go +++ b/database/gdb/gdb_schema.go @@ -40,6 +40,14 @@ func (s *Schema) Table(table string) *Model { } else { m = s.db.Table(table) } + // Do not change the schema of the original db, + // it here creates a new db and changes its schema. + db, err := New(m.db.GetGroup()) + if err != nil { + panic(err) + } + db.SetSchema(s.schema) + m.db = db m.schema = s.schema return m } diff --git a/util/gutil/gutil_map.go b/util/gutil/gutil_map.go index 960b7282b..48b6b3a83 100644 --- a/util/gutil/gutil_map.go +++ b/util/gutil/gutil_map.go @@ -30,7 +30,7 @@ func CopyMap(data map[string]interface{}) (copy map[string]interface{}) { // cases or chars '-'/'_'/'.'/' '. // // Note that this function might be of low performance. -func MapPossibleItemByKey(data map[string]interface{}, key string) (string, interface{}) { +func MapPossibleItemByKey(data map[string]interface{}, key string) (foundKey string, foundValue interface{}) { if v, ok := data[key]; ok { return key, v } @@ -47,3 +47,12 @@ func MapPossibleItemByKey(data map[string]interface{}, key string) (string, inte } return "", nil } + +// MapContainsPossibleKey checks if the given is contained in given map . +// It checks the key with or without cases or chars '-'/'_'/'.'/' '. +func MapContainsPossibleKey(data map[string]interface{}, key string) bool { + if k, _ := MapPossibleItemByKey(data, key); k != "" { + return true + } + return false +}