From 2acdf4bb4758f7d07e0c1a925fea607e347f2679 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 6 Feb 2024 10:21:23 +0800 Subject: [PATCH] add field type detection for soft time field like `created_at/updated_at/deleted_at` to support unix timestamp or bool deleting table field (#3293) --- ...=> mysql_z_unit_feature_soft_time_test.go} | 304 ++++++++++++- database/gdb/gdb.go | 1 + database/gdb/gdb_core_structure.go | 4 +- database/gdb/gdb_model.go | 69 +-- database/gdb/gdb_model_delete.go | 15 +- database/gdb/gdb_model_insert.go | 26 +- database/gdb/gdb_model_select.go | 2 +- database/gdb/gdb_model_soft_time.go | 423 ++++++++++++++++++ database/gdb/gdb_model_time.go | 181 -------- database/gdb/gdb_model_update.go | 12 +- os/gcache/gcache.go | 2 +- 11 files changed, 791 insertions(+), 248 deletions(-) rename contrib/drivers/mysql/{mysql_z_unit_feature_time_maintain_test.go => mysql_z_unit_feature_soft_time_test.go} (76%) create mode 100644 database/gdb/gdb_model_soft_time.go delete mode 100644 database/gdb/gdb_model_time.go diff --git a/contrib/drivers/mysql/mysql_z_unit_feature_time_maintain_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go similarity index 76% rename from contrib/drivers/mysql/mysql_z_unit_feature_time_maintain_test.go rename to contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go index 13aac68b4..10f12918b 100644 --- a/contrib/drivers/mysql/mysql_z_unit_feature_time_maintain_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go @@ -11,14 +11,15 @@ import ( "testing" "time" + "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" ) // CreateAt/UpdateAt/DeleteAt. -func Test_SoftCreateUpdateDeleteTimeMicroSecond(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() +func Test_SoftTime_CreateUpdateDelete1(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -151,8 +152,8 @@ CREATE TABLE %s ( } // CreateAt/UpdateAt/DeleteAt. -func Test_SoftCreateUpdateDeleteTimeSecond(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() +func Test_SoftTime_CreateUpdateDelete2(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -285,8 +286,8 @@ CREATE TABLE %s ( } // CreatedAt/UpdatedAt/DeletedAt. -func Test_SoftCreatedUpdatedDeletedTime_Map(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() +func Test_SoftTime_CreatedUpdatedDeleted_Map(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -419,8 +420,8 @@ CREATE TABLE %s ( } // CreatedAt/UpdatedAt/DeletedAt. -func Test_SoftCreatedUpdatedDeletedTime_Struct(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() +func Test_SoftTime_CreatedUpdatedDeleted_Struct(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -560,7 +561,7 @@ CREATE TABLE %s ( } func Test_SoftUpdateTime(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -600,7 +601,7 @@ CREATE TABLE %s ( } func Test_SoftUpdateTime_WithDO(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -657,7 +658,7 @@ CREATE TABLE %s ( } func Test_SoftDelete(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -796,7 +797,7 @@ CREATE TABLE %s ( } func Test_SoftDelete_WhereAndOr(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -838,7 +839,7 @@ CREATE TABLE %s ( } func Test_CreateUpdateTime_Struct(t *testing.T) { - table := "time_test_table_" + gtime.TimestampNanoStr() + table := "soft_time_test_table_" + gtime.TimestampNanoStr() if _, err := db.Exec(ctx, fmt.Sprintf(` CREATE TABLE %s ( id int(11) NOT NULL, @@ -989,3 +990,280 @@ CREATE TABLE %s ( t.Assert(i, 0) }) } + +func Test_SoftTime_CreateUpdateDelete_UnixTimestamp(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at int(11) DEFAULT NULL, + update_at int(11) DEFAULT NULL, + delete_at int(11) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.Assert(one["delete_at"].Int64(), 0) + t.Assert(len(one["create_at"].String()), 10) + t.Assert(len(one["update_at"].String()), 10) + }) + + // sleep some seconds to make update time greater than create time. + time.Sleep(2 * time.Second) + + // update + gtest.C(t, func(t *gtest.T) { + // update: map + dataInsert := g.Map{ + "name": "name_11", + } + r, err := db.Model(table).Data(dataInsert).WherePri(1).Update() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_11") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.Assert(one["delete_at"].Int64(), 0) + t.Assert(len(one["create_at"].String()), 10) + t.Assert(len(one["update_at"].String()), 10) + + var ( + lastCreateTime = one["create_at"].Int64() + lastUpdateTime = one["update_at"].Int64() + ) + + time.Sleep(2 * time.Second) + + // update: string + r, err = db.Model(table).Data("name='name_111'").WherePri(1).Update() + t.AssertNil(err) + n, _ = r.RowsAffected() + t.Assert(n, 1) + + one, err = db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_111") + t.Assert(one["create_at"].Int64(), lastCreateTime) + t.AssertGT(one["update_at"].Int64(), lastUpdateTime) + t.Assert(one["delete_at"].Int64(), 0) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_111") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.AssertGT(one["delete_at"].Int64(), 0) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Bool_Deleted(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at int(11) DEFAULT NULL, + update_at int(11) DEFAULT NULL, + delete_at bit(1) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + //db.SetDebug(true) + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.Assert(one["delete_at"].Int64(), 0) + t.Assert(len(one["create_at"].String()), 10) + t.Assert(len(one["update_at"].String()), 10) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.Assert(one["delete_at"].Int64(), 1) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Option_SoftTimeTypeTimestampMilli(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at bigint(19) unsigned DEFAULT NULL, + update_at bigint(19) unsigned DEFAULT NULL, + delete_at bit(1) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + var softTimeOption = gdb.SoftTimeOption{ + SoftTimeType: gdb.SoftTimeTypeTimestampMilli, + } + + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).SoftTime(softTimeOption).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.Assert(len(one["create_at"].String()), 13) + t.Assert(len(one["update_at"].String()), 13) + t.Assert(one["delete_at"].Int64(), 0) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.Assert(one["delete_at"].Int64(), 1) + }) +} + +func Test_SoftTime_CreateUpdateDelete_Option_SoftTimeTypeTimestampNano(t *testing.T) { + table := "soft_time_test_table_" + gtime.TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` +CREATE TABLE %s ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at bigint(19) unsigned DEFAULT NULL, + update_at bigint(19) unsigned DEFAULT NULL, + delete_at bit(1) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table)); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + var softTimeOption = gdb.SoftTimeOption{ + SoftTimeType: gdb.SoftTimeTypeTimestampNano, + } + + // insert + gtest.C(t, func(t *gtest.T) { + dataInsert := g.Map{ + "id": 1, + "name": "name_1", + } + r, err := db.Model(table).SoftTime(softTimeOption).Data(dataInsert).Insert() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.Assert(len(one["create_at"].String()), 19) + t.Assert(len(one["update_at"].String()), 19) + t.Assert(one["delete_at"].Int64(), 0) + }) + + // delete + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).Delete() + t.AssertNil(err) + n, _ := r.RowsAffected() + t.Assert(n, 1) + + one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One() + t.AssertNil(err) + t.Assert(len(one), 0) + + one, err = db.Model(table).Unscoped().WherePri(1).One() + t.AssertNil(err) + t.Assert(one["name"].String(), "name_1") + t.AssertGT(one["create_at"].Int64(), 0) + t.AssertGT(one["update_at"].Int64(), 0) + t.Assert(one["delete_at"].Int64(), 1) + }) +} diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index e0ddc0329..00e0a2d6e 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -425,6 +425,7 @@ const ( type LocalType string const ( + LocalTypeUndefined LocalType = "" LocalTypeString LocalType = "string" LocalTypeDate LocalType = "date" LocalTypeDatetime LocalType = "datetime" diff --git a/database/gdb/gdb_core_structure.go b/database/gdb/gdb_core_structure.go index 161825094..81d6971b1 100644 --- a/database/gdb/gdb_core_structure.go +++ b/database/gdb/gdb_core_structure.go @@ -299,7 +299,9 @@ func (c *Core) CheckLocalTypeForField(ctx context.Context, fieldType string, fie // ConvertValueForLocal converts value to local Golang type of value according field type name from database. // The parameter `fieldType` is in lower case, like: // `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc. -func (c *Core) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { +func (c *Core) ConvertValueForLocal( + ctx context.Context, fieldType string, fieldValue interface{}, +) (interface{}, error) { // If there's no type retrieved, it returns the `fieldValue` directly // to use its original data type, as `fieldValue` is type of interface{}. if fieldType == "" { diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index dc8f596ba..f85406364 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -17,40 +17,41 @@ import ( // Model is core struct implementing the DAO for ORM. type Model struct { - db DB // Underlying DB interface. - tx TX // Underlying TX interface. - rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model. - schema string // Custom database schema. - linkType int // Mark for operation on master or slave. - tablesInit string // Table names when model initialization. - 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. - withAll bool // Enable model association operations on all objects that have "with" tag in the struct. - extraArgs []interface{} // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver. - whereBuilder *WhereBuilder // Condition builder for where operation. - groupBy string // Used for "group by" statement. - orderBy string // Used for "order by" statement. - having []interface{} // Used for "having..." statement. - start int // Used for "select ... start, limit ..." statement. - limit int // Used for "select ... start, limit ..." statement. - option int // Option for extra operation features. - offset int // Offset statement for some databases grammar. - partition string // Partition table partition name. - data interface{} // Data for operation, which can be type of map/[]map/struct/*struct/string, etc. - batch int // Batch number for batch Insert/Replace/Save operations. - filter bool // Filter data and where key-value pairs according to the fields of the table. - distinct string // Force the query to only return distinct results. - lockInfo string // Lock for update or in shared lock. - cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage. - cacheOption CacheOption // Cache option for query statement. - hookHandler HookHandler // Hook functions for model hook feature. - unscoped bool // Disables soft deleting features when select/delete operations. - safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. - onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement. - onDuplicateEx interface{} // onDuplicateEx is used for excluding some columns ON "DUPLICATE KEY UPDATE" statement. - tableAliasMap map[string]string // Table alias to true table name, usually used in join statements. + db DB // Underlying DB interface. + tx TX // Underlying TX interface. + rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model. + schema string // Custom database schema. + linkType int // Mark for operation on master or slave. + tablesInit string // Table names when model initialization. + 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. + withAll bool // Enable model association operations on all objects that have "with" tag in the struct. + extraArgs []interface{} // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver. + whereBuilder *WhereBuilder // Condition builder for where operation. + groupBy string // Used for "group by" statement. + orderBy string // Used for "order by" statement. + having []interface{} // Used for "having..." statement. + start int // Used for "select ... start, limit ..." statement. + limit int // Used for "select ... start, limit ..." statement. + option int // Option for extra operation features. + offset int // Offset statement for some databases grammar. + partition string // Partition table partition name. + data interface{} // Data for operation, which can be type of map/[]map/struct/*struct/string, etc. + batch int // Batch number for batch Insert/Replace/Save operations. + filter bool // Filter data and where key-value pairs according to the fields of the table. + distinct string // Force the query to only return distinct results. + lockInfo string // Lock for update or in shared lock. + cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage. + cacheOption CacheOption // Cache option for query statement. + hookHandler HookHandler // Hook functions for model hook feature. + unscoped bool // Disables soft deleting features when select/delete operations. + safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. + onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement. + onDuplicateEx interface{} // onDuplicateEx is used for excluding some columns ON "DUPLICATE KEY UPDATE" statement. + tableAliasMap map[string]string // Table alias to true table name, usually used in join statements. + softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model. } // ModelHandler is a function that handles given Model and returns a new Model that is custom modified. diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index 26b23759c..b6a32c524 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -8,13 +8,11 @@ package gdb import ( "database/sql" - "fmt" + "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/text/gstr" - - "github.com/gogf/gf/v2/os/gtime" ) // Delete does "DELETE FROM ... " statement for the model. @@ -31,9 +29,11 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { } }() var ( - fieldNameDelete = m.getSoftFieldNameDeleted("", m.tablesInit) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra + fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetFieldNameAndTypeForDelete( + ctx, "", m.tablesInit, + ) ) if m.unscoped { fieldNameDelete = "" @@ -52,6 +52,9 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { // Soft deleting. if fieldNameDelete != "" { + dataHolder, dataValue := m.softTimeMaintainer().GetDataByFieldNameAndTypeForDelete( + ctx, "", fieldNameDelete, fieldTypeDelete, + ) in := &HookUpdateInput{ internalParamHookUpdate: internalParamHookUpdate{ internalParamHook: internalParamHook{ @@ -61,9 +64,9 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { }, Model: m, Table: m.tables, - Data: fmt.Sprintf(`%s=?`, m.db.GetCore().QuoteString(fieldNameDelete)), + Data: dataHolder, Condition: conditionStr, - Args: append([]interface{}{gtime.Now()}, conditionArgs...), + Args: append([]interface{}{dataValue}, conditionArgs...), } return in.Next(ctx) } diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index 80fe9ac65..a470d3d1c 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -15,7 +15,6 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/reflection" - "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gutil" @@ -243,10 +242,11 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio return nil, gerror.NewCode(gcode.CodeMissingParameter, "inserting into table with empty data") } var ( - list List - now = gtime.Now() - fieldNameCreate = m.getSoftFieldNameCreated("", m.tablesInit) - fieldNameUpdate = m.getSoftFieldNameUpdated("", m.tablesInit) + list List + stm = m.softTimeMaintainer() + fieldNameCreate, fieldTypeCreate = stm.GetFieldNameAndTypeForCreate(ctx, "", m.tablesInit) + fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate(ctx, "", m.tablesInit) + fieldNameDelete, fieldTypeDelete = stm.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit) ) // m.data was already converted to type List/Map by function Data newData, err := m.filterDataForInsertOrUpdate(m.data) @@ -270,10 +270,22 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio if !m.unscoped && (fieldNameCreate != "" || fieldNameUpdate != "") { for k, v := range list { if fieldNameCreate != "" { - v[fieldNameCreate] = now + fieldCreateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate, false) + if fieldCreateValue != nil { + v[fieldNameCreate] = fieldCreateValue + } } if fieldNameUpdate != "" { - v[fieldNameUpdate] = now + fieldUpdateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false) + if fieldUpdateValue != nil { + v[fieldNameUpdate] = fieldUpdateValue + } + } + if fieldNameDelete != "" { + fieldDeleteValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeDelete, true) + if fieldDeleteValue != nil { + v[fieldNameDelete] = fieldDeleteValue + } } list[k] = v } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 5a37968f7..69bfd38c3 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -730,7 +730,7 @@ func (m *Model) formatCondition( } // WHERE conditionWhere, conditionArgs = m.whereBuilder.Build() - softDeletingCondition := m.getConditionForSoftDeleting() + softDeletingCondition := m.softTimeMaintainer().GetWhereConditionForDelete(ctx) if m.rawSql != "" && conditionWhere != "" { if gstr.ContainsI(m.rawSql, " WHERE ") { conditionWhere = " AND " + conditionWhere diff --git a/database/gdb/gdb_model_soft_time.go b/database/gdb/gdb_model_soft_time.go new file mode 100644 index 000000000..e9ac66a09 --- /dev/null +++ b/database/gdb/gdb_model_soft_time.go @@ -0,0 +1,423 @@ +// 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 + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/intlog" + "github.com/gogf/gf/v2/os/gcache" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/gutil" +) + +// SoftTimeType custom defines the soft time field type. +type SoftTimeType int + +const ( + SoftTimeTypeAuto SoftTimeType = 0 // (Default)Auto detect the field type by table field type. + SoftTimeTypeTime SoftTimeType = 1 // Using datetime as the field value. + SoftTimeTypeTimestamp SoftTimeType = 2 // In unix seconds. + SoftTimeTypeTimestampMilli SoftTimeType = 3 // In unix milliseconds. + SoftTimeTypeTimestampMicro SoftTimeType = 4 // In unix microseconds. + SoftTimeTypeTimestampNano SoftTimeType = 5 // In unix nanoseconds. +) + +// SoftTimeOption is the option to customize soft time feature for Model. +type SoftTimeOption struct { + SoftTimeType SoftTimeType // The value type for soft time field. +} + +type softTimeMaintainer struct { + *Model +} + +type iSoftTimeMaintainer interface { + GetFieldNameAndTypeForCreate( + ctx context.Context, schema string, table string, + ) (fieldName string, fieldType LocalType) + + GetFieldNameAndTypeForUpdate( + ctx context.Context, schema string, table string, + ) (fieldName string, fieldType LocalType) + + GetFieldNameAndTypeForDelete( + ctx context.Context, schema string, table string, + ) (fieldName string, fieldType LocalType) + + GetValueByFieldTypeForCreateOrUpdate( + ctx context.Context, fieldType LocalType, isDeletedField bool, + ) (dataValue any) + + GetDataByFieldNameAndTypeForDelete( + ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, + ) (dataHolder string, dataValue any) + + GetWhereConditionForDelete(ctx context.Context) string +} + +// getSoftFieldNameAndTypeCacheItem is the internal struct for storing create/update/delete fields. +type getSoftFieldNameAndTypeCacheItem struct { + FieldName string + FieldType LocalType +} + +var ( + // Default field names of table for automatic-filled for record creating. + createdFieldNames = []string{"created_at", "create_at"} + // Default field names of table for automatic-filled for record updating. + updatedFieldNames = []string{"updated_at", "update_at"} + // Default field names of table for automatic-filled for record deleting. + deletedFieldNames = []string{"deleted_at", "delete_at"} +) + +// SoftTime sets the SoftTimeOption to customize soft time feature for Model. +func (m *Model) SoftTime(option SoftTimeOption) *Model { + model := m.getModel() + model.softTimeOption = option + return model +} + +// Unscoped disables the soft time feature for insert, update and delete operations. +func (m *Model) Unscoped() *Model { + model := m.getModel() + model.unscoped = true + return model +} + +func (m *Model) softTimeMaintainer() iSoftTimeMaintainer { + return &softTimeMaintainer{ + m, + } +} + +// GetFieldNameAndTypeForCreate 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 *softTimeMaintainer) GetFieldNameAndTypeForCreate( + ctx context.Context, schema string, table string, +) (fieldName string, fieldType LocalType) { + // It checks whether this feature disabled. + if m.db.GetConfig().TimeMaintainDisabled { + return "", LocalTypeUndefined + } + tableName := "" + if table != "" { + tableName = table + } else { + tableName = m.tablesInit + } + config := m.db.GetConfig() + if config.CreatedAt != "" { + return m.getSoftFieldNameAndType( + ctx, schema, tableName, []string{config.CreatedAt}, + ) + } + return m.getSoftFieldNameAndType( + ctx, schema, tableName, createdFieldNames, + ) +} + +// GetFieldNameAndTypeForUpdate 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 *softTimeMaintainer) GetFieldNameAndTypeForUpdate( + ctx context.Context, schema string, table string, +) (fieldName string, fieldType LocalType) { + // It checks whether this feature disabled. + if m.db.GetConfig().TimeMaintainDisabled { + return "", LocalTypeUndefined + } + tableName := "" + if table != "" { + tableName = table + } else { + tableName = m.tablesInit + } + config := m.db.GetConfig() + if config.UpdatedAt != "" { + return m.getSoftFieldNameAndType( + ctx, schema, tableName, []string{config.UpdatedAt}, + ) + } + return m.getSoftFieldNameAndType( + ctx, schema, tableName, updatedFieldNames, + ) +} + +// GetFieldNameAndTypeForDelete 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 *softTimeMaintainer) GetFieldNameAndTypeForDelete( + ctx context.Context, schema string, table string, +) (fieldName string, fieldType LocalType) { + // It checks whether this feature disabled. + if m.db.GetConfig().TimeMaintainDisabled { + return "", LocalTypeUndefined + } + tableName := "" + if table != "" { + tableName = table + } else { + tableName = m.tablesInit + } + config := m.db.GetConfig() + if config.DeletedAt != "" { + return m.getSoftFieldNameAndType( + ctx, schema, tableName, []string{config.DeletedAt}, + ) + } + return m.getSoftFieldNameAndType( + ctx, schema, tableName, deletedFieldNames, + ) +} + +// getSoftFieldName retrieves and returns the field name of the table for possible key. +func (m *softTimeMaintainer) getSoftFieldNameAndType( + ctx context.Context, + schema string, table string, checkFiledNames []string, +) (fieldName string, fieldType LocalType) { + var ( + cacheKey = fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%p`, schema, table, checkFiledNames) + cacheDuration = gcache.DurationNoExpire + cacheFunc = func(ctx context.Context) (value interface{}, err error) { + // Ignore the error from TableFields. + fieldsMap, _ := m.TableFields(table, schema) + if len(fieldsMap) > 0 { + for _, checkFiledName := range checkFiledNames { + fieldName, _ = gutil.MapPossibleItemByKey( + gconv.Map(fieldsMap), checkFiledName, + ) + if fieldName != "" { + fieldType, _ = m.db.CheckLocalTypeForField( + ctx, fieldsMap[fieldName].Type, nil, + ) + var cacheItem = getSoftFieldNameAndTypeCacheItem{ + FieldName: fieldName, + FieldType: fieldType, + } + return cacheItem, nil + } + } + } + return + } + ) + result, err := m.db.GetCache().GetOrSetFunc(ctx, cacheKey, cacheFunc, cacheDuration) + if err != nil { + intlog.Error(ctx, err) + } + if result != nil { + var cacheItem = result.Val().(getSoftFieldNameAndTypeCacheItem) + fieldName = cacheItem.FieldName + fieldType = cacheItem.FieldType + } + return +} + +// GetWhereConditionForDelete retrieves and returns the condition string for soft deleting. +// It supports multiple tables string like: +// "user u, user_detail ud" +// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)" +// "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)" +// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)". +func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) string { + if m.unscoped { + return "" + } + conditionArray := garray.NewStrArray() + if gstr.Contains(m.tables, " JOIN ") { + // Base table. + tableMatch, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables) + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, tableMatch[1])) + // Multiple joined tables, exclude the sub query sql which contains char '(' and ')'. + tableMatches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables) + for _, match := range tableMatches { + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, match[1])) + } + } + if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") { + // Multiple base tables. + for _, s := range gstr.SplitAndTrim(m.tables, ",") { + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, s)) + } + } + conditionArray.FilterEmpty() + if conditionArray.Len() > 0 { + return conditionArray.Join(" AND ") + } + // Only one table. + fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit) + if fieldName != "" { + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType) + } + return "" +} + +// getConditionOfTableStringForSoftDeleting does something as its name describes. +// Examples for `s`: +// - `test`.`demo` as b +// - `test`.`demo` b +// - `demo` +// - demo +func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string { + var ( + table string + schema string + array1 = gstr.SplitAndTrim(s, " ") + array2 = gstr.SplitAndTrim(array1[0], ".") + ) + if len(array2) >= 2 { + table = array2[1] + schema = array2[0] + } else { + table = array2[0] + } + fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table) + if fieldName == "" { + return "" + } + if len(array1) >= 3 { + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType) + } + if len(array1) >= 2 { + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType) + } + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType) +} + +// GetDataByFieldNameAndTypeForDelete creates and returns the placeholder and value for +// specified field name and type in soft-deleting scenario. +func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete( + ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, +) (dataHolder string, dataValue any) { + var ( + quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix) + quotedFieldName = m.db.GetCore().QuoteWord(fieldName) + ) + if quotedFieldPrefix != "" { + quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName) + } + dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName) + dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false) + return +} + +func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting( + ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, +) string { + var ( + quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix) + quotedFieldName = m.db.GetCore().QuoteWord(fieldName) + ) + if quotedFieldPrefix != "" { + quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName) + } + switch m.softTimeOption.SoftTimeType { + case SoftTimeTypeAuto: + switch fieldType { + case LocalTypeDate, LocalTypeDatetime: + return fmt.Sprintf(`%s IS NULL`, quotedFieldName) + case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeBool: + return fmt.Sprintf(`%s=0`, quotedFieldName) + default: + intlog.Errorf( + ctx, + `invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`, + fieldType, fieldName, fieldPrefix, + ) + } + + case SoftTimeTypeTime: + return fmt.Sprintf(`%s IS NULL`, quotedFieldName) + + default: + return fmt.Sprintf(`%s=0`, quotedFieldName) + } + return "" +} + +// GetValueByFieldTypeForCreateOrUpdate creates and returns the value for specified field type, +// usually for creating or updating operations. +func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate( + ctx context.Context, fieldType LocalType, isDeletedField bool, +) any { + var value any + if isDeletedField { + switch fieldType { + case LocalTypeDate, LocalTypeDatetime: + value = nil + default: + value = 0 + } + return value + } + switch m.softTimeOption.SoftTimeType { + case SoftTimeTypeAuto: + switch fieldType { + case LocalTypeDate, LocalTypeDatetime: + value = gtime.Now() + case LocalTypeInt, LocalTypeUint, LocalTypeInt64: + value = gtime.Timestamp() + case LocalTypeBool: + value = 1 + default: + intlog.Errorf( + ctx, + `invalid field type "%s" for soft deleting data`, + fieldType, + ) + } + + default: + switch fieldType { + case LocalTypeBool: + value = 1 + default: + value = m.createValueBySoftTimeOption(isDeletedField) + } + } + return value +} + +func (m *softTimeMaintainer) createValueBySoftTimeOption(isDeletedField bool) any { + var value any + if isDeletedField { + switch m.softTimeOption.SoftTimeType { + case SoftTimeTypeTime: + value = nil + default: + value = 0 + } + return value + } + switch m.softTimeOption.SoftTimeType { + case SoftTimeTypeTime: + value = gtime.Now() + case SoftTimeTypeTimestamp: + value = gtime.Timestamp() + case SoftTimeTypeTimestampMilli: + value = gtime.TimestampMilli() + case SoftTimeTypeTimestampMicro: + value = gtime.TimestampMicro() + case SoftTimeTypeTimestampNano: + value = gtime.TimestampNano() + default: + panic(gerror.NewCodef( + gcode.CodeInternalPanic, + `unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType, + )) + } + return value +} diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go deleted file mode 100644 index 76333ba65..000000000 --- a/database/gdb/gdb_model_time.go +++ /dev/null @@ -1,181 +0,0 @@ -// 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 - -import ( - "fmt" - - "github.com/gogf/gf/v2/container/garray" - "github.com/gogf/gf/v2/text/gregex" - "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" -) - -var ( - createdFieldNames = []string{"created_at", "create_at"} // Default field names of table for automatic-filled created datetime. - updatedFieldNames = []string{"updated_at", "update_at"} // Default field names of table for automatic-filled updated datetime. - deletedFieldNames = []string{"deleted_at", "delete_at"} // Default field names of table for automatic-filled deleted datetime. -) - -// Unscoped disables the auto-update time feature for insert, update and delete options. -func (m *Model) Unscoped() *Model { - model := m.getModel() - model.unscoped = true - return model -} - -// 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) getSoftFieldNameCreated(schema string, table string) string { - // It checks whether this feature disabled. - if m.db.GetConfig().TimeMaintainDisabled { - return "" - } - tableName := "" - if table != "" { - tableName = table - } else { - tableName = m.tablesInit - } - config := m.db.GetConfig() - if config.CreatedAt != "" { - return m.getSoftFieldName(schema, tableName, []string{config.CreatedAt}) - } - return m.getSoftFieldName(schema, tableName, createdFieldNames) -} - -// 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) getSoftFieldNameUpdated(schema string, table string) (field string) { - // It checks whether this feature disabled. - if m.db.GetConfig().TimeMaintainDisabled { - return "" - } - tableName := "" - if table != "" { - tableName = table - } else { - tableName = m.tablesInit - } - config := m.db.GetConfig() - if config.UpdatedAt != "" { - return m.getSoftFieldName(schema, tableName, []string{config.UpdatedAt}) - } - return m.getSoftFieldName(schema, tableName, updatedFieldNames) -} - -// 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) getSoftFieldNameDeleted(schema string, table string) (field string) { - // It checks whether this feature disabled. - if m.db.GetConfig().TimeMaintainDisabled { - return "" - } - tableName := "" - if table != "" { - tableName = table - } else { - tableName = m.tablesInit - } - config := m.db.GetConfig() - if config.DeletedAt != "" { - return m.getSoftFieldName(schema, tableName, []string{config.DeletedAt}) - } - return m.getSoftFieldName(schema, tableName, deletedFieldNames) -} - -// getSoftFieldName retrieves and returns the field name of the table for possible key. -func (m *Model) getSoftFieldName(schema string, table string, keys []string) (field string) { - // Ignore the error from TableFields. - fieldsMap, _ := m.TableFields(table, schema) - if len(fieldsMap) > 0 { - for _, key := range keys { - field, _ = gutil.MapPossibleItemByKey( - gconv.Map(fieldsMap), key, - ) - if field != "" { - return - } - } - } - return -} - -// getConditionForSoftDeleting retrieves and returns the condition string for soft deleting. -// It supports multiple tables string like: -// "user u, user_detail ud" -// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)" -// "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)" -// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)". -func (m *Model) getConditionForSoftDeleting() string { - if m.unscoped { - return "" - } - conditionArray := garray.NewStrArray() - if gstr.Contains(m.tables, " JOIN ") { - // Base table. - match, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables) - conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1])) - // Multiple joined tables, exclude the sub query sql which contains char '(' and ')'. - matches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables) - for _, match := range matches { - conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1])) - } - } - if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") { - // Multiple base tables. - for _, s := range gstr.SplitAndTrim(m.tables, ",") { - conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(s)) - } - } - conditionArray.FilterEmpty() - if conditionArray.Len() > 0 { - return conditionArray.Join(" AND ") - } - // Only one table. - if fieldName := m.getSoftFieldNameDeleted("", m.tablesInit); fieldName != "" { - return fmt.Sprintf(`%s IS NULL`, m.db.GetCore().QuoteWord(fieldName)) - } - return "" -} - -// getConditionOfTableStringForSoftDeleting does something as its name describes. -// Examples for `s`: -// - `test`.`demo` as b -// - `test`.`demo` b -// - `demo` -// - demo -func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string { - var ( - field string - table string - schema string - array1 = gstr.SplitAndTrim(s, " ") - array2 = gstr.SplitAndTrim(array1[0], ".") - ) - if len(array2) >= 2 { - table = array2[1] - schema = array2[0] - } else { - table = array2[0] - } - field = m.getSoftFieldNameDeleted(schema, table) - if field == "" { - return "" - } - if len(array1) >= 3 { - return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[2]), m.db.GetCore().QuoteWord(field)) - } - if len(array1) >= 2 { - return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[1]), m.db.GetCore().QuoteWord(field)) - } - return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(table), m.db.GetCore().QuoteWord(field)) -} diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index 32c749670..4f2f11cb3 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -16,7 +16,6 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/reflection" - "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" ) @@ -46,11 +45,14 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro return nil, gerror.NewCode(gcode.CodeMissingParameter, "updating table with empty data") } var ( + stm = m.softTimeMaintainer() updateData = m.data reflectInfo = reflection.OriginTypeAndKind(updateData) - fieldNameUpdate = m.getSoftFieldNameUpdated("", m.tablesInit) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra + fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate( + ctx, "", m.tablesInit, + ) ) if m.unscoped { fieldNameUpdate = "" @@ -61,7 +63,8 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro var dataMap = anyValueToMapBeforeToRecord(m.data) // Automatically update the record updating time. if fieldNameUpdate != "" { - dataMap[fieldNameUpdate] = gtime.Now() + dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false) + dataMap[fieldNameUpdate] = dataValue } updateData = dataMap @@ -69,9 +72,10 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro updates := gconv.String(m.data) // Automatically update the record updating time. if fieldNameUpdate != "" { + dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false) if fieldNameUpdate != "" && !gstr.Contains(updates, fieldNameUpdate) { updates += fmt.Sprintf(`,%s=?`, fieldNameUpdate) - conditionArgs = append([]interface{}{gtime.Now()}, conditionArgs...) + conditionArgs = append([]interface{}{dataValue}, conditionArgs...) } } updateData = updates diff --git a/os/gcache/gcache.go b/os/gcache/gcache.go index 749218a1e..645508b78 100644 --- a/os/gcache/gcache.go +++ b/os/gcache/gcache.go @@ -20,7 +20,7 @@ import ( type Func func(ctx context.Context) (value interface{}, err error) const ( - DurationNoExpire = 0 // Expire duration that never expires. + DurationNoExpire = time.Duration(0) // Expire duration that never expires. ) // Default cache object.