From be2553d8cdd800b722f2992ad58a263cda6761f1 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 30 Jan 2024 22:02:58 +0800 Subject: [PATCH 1/7] add field type auto detection for soft time field --- database/gdb/gdb.go | 1 + database/gdb/gdb_core_structure.go | 4 +- database/gdb/gdb_model_delete.go | 11 +- database/gdb/gdb_model_insert.go | 12 +-- database/gdb/gdb_model_select.go | 2 +- database/gdb/gdb_model_time.go | 159 ++++++++++++++++++++++------- database/gdb/gdb_model_update.go | 5 +- 7 files changed, 141 insertions(+), 53 deletions(-) diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index e0ddc0329bc..00e0a2d6e4b 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 161825094d8..81d6971b119 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_delete.go b/database/gdb/gdb_model_delete.go index 26b23759cc9..ab010cb84f5 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,7 +29,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { } }() var ( - fieldNameDelete = m.getSoftFieldNameDeleted("", m.tablesInit) + fieldNameDelete, fieldTypeDelete = m.getSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra ) @@ -52,6 +50,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { // Soft deleting. if fieldNameDelete != "" { + dataHolder, dataValue := m.getDataByFieldNameAndTypeForSoftDeleting(ctx, "", fieldNameDelete, fieldTypeDelete) in := &HookUpdateInput{ internalParamHookUpdate: internalParamHookUpdate{ internalParamHook: internalParamHook{ @@ -61,9 +60,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 bc6b8aa82be..5f67685bdb7 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,9 @@ 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 + fieldNameCreate, fieldTypeCreate = m.getSoftFieldNameAndTypeCreated(ctx, "", m.tablesInit) + fieldNameUpdate, fieldTypeUpdate = m.getSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) ) newData, err := m.filterDataForInsertOrUpdate(m.data) if err != nil { @@ -309,10 +307,10 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio if !m.unscoped && (fieldNameCreate != "" || fieldNameUpdate != "") { for k, v := range list { if fieldNameCreate != "" { - v[fieldNameCreate] = now + v[fieldNameCreate] = m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate) } if fieldNameUpdate != "" { - v[fieldNameUpdate] = now + v[fieldNameUpdate] = m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) } list[k] = v } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 5a37968f7c1..f90a86d7c1e 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.getConditionForSoftDeleting(ctx) if m.rawSql != "" && conditionWhere != "" { if gstr.ContainsI(m.rawSql, " WHERE ") { conditionWhere = " AND " + conditionWhere diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go index 76333ba6556..3381285d9d2 100644 --- a/database/gdb/gdb_model_time.go +++ b/database/gdb/gdb_model_time.go @@ -7,9 +7,12 @@ package gdb import ( + "context" "fmt" "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/internal/intlog" + "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" @@ -29,13 +32,15 @@ func (m *Model) Unscoped() *Model { return model } -// getSoftFieldNameCreate checks and returns the field name for record creating time. +// getSoftFieldNameAndTypeCreated 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 { +func (m *Model) getSoftFieldNameAndTypeCreated( + ctx context.Context, schema string, table string, +) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. if m.db.GetConfig().TimeMaintainDisabled { - return "" + return "", LocalTypeUndefined } tableName := "" if table != "" { @@ -45,18 +50,24 @@ func (m *Model) getSoftFieldNameCreated(schema string, table string) string { } config := m.db.GetConfig() if config.CreatedAt != "" { - return m.getSoftFieldName(schema, tableName, []string{config.CreatedAt}) + return m.getSoftFieldNameAndType( + ctx, schema, tableName, []string{config.CreatedAt}, + ) } - return m.getSoftFieldName(schema, tableName, createdFieldNames) + return m.getSoftFieldNameAndType( + ctx, schema, tableName, createdFieldNames, + ) } -// getSoftFieldNameUpdate checks and returns the field name for record updating time. +// getSoftFieldNameAndTypeUpdated 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) { +func (m *Model) getSoftFieldNameAndTypeUpdated( + ctx context.Context, schema string, table string, +) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. if m.db.GetConfig().TimeMaintainDisabled { - return "" + return "", LocalTypeUndefined } tableName := "" if table != "" { @@ -66,18 +77,24 @@ func (m *Model) getSoftFieldNameUpdated(schema string, table string) (field stri } config := m.db.GetConfig() if config.UpdatedAt != "" { - return m.getSoftFieldName(schema, tableName, []string{config.UpdatedAt}) + return m.getSoftFieldNameAndType( + ctx, schema, tableName, []string{config.UpdatedAt}, + ) } - return m.getSoftFieldName(schema, tableName, updatedFieldNames) + return m.getSoftFieldNameAndType( + ctx, schema, tableName, updatedFieldNames, + ) } -// getSoftFieldNameDelete checks and returns the field name for record deleting time. +// getSoftFieldNameAndTypeDeleted 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) { +func (m *Model) getSoftFieldNameAndTypeDeleted( + ctx context.Context, schema string, table string, +) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. if m.db.GetConfig().TimeMaintainDisabled { - return "" + return "", LocalTypeUndefined } tableName := "" if table != "" { @@ -87,21 +104,31 @@ func (m *Model) getSoftFieldNameDeleted(schema string, table string) (field stri } config := m.db.GetConfig() if config.DeletedAt != "" { - return m.getSoftFieldName(schema, tableName, []string{config.DeletedAt}) + return m.getSoftFieldNameAndType( + ctx, schema, tableName, []string{config.DeletedAt}, + ) } - return m.getSoftFieldName(schema, tableName, deletedFieldNames) + return m.getSoftFieldNameAndType( + ctx, 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) { +func (m *Model) getSoftFieldNameAndType( + ctx context.Context, + schema string, table string, checkFiledNames []string, +) (fieldName string, fieldType LocalType) { // 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, + for _, checkFiledName := range checkFiledNames { + fieldName, _ = gutil.MapPossibleItemByKey( + gconv.Map(fieldsMap), checkFiledName, ) - if field != "" { + if fieldName != "" { + fieldType, _ = m.db.CheckLocalTypeForField( + ctx, fieldsMap[fieldName].Type, nil, + ) return } } @@ -115,25 +142,25 @@ func (m *Model) getSoftFieldName(schema string, table string, keys []string) (fi // "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 { +func (m *Model) getConditionForSoftDeleting(ctx context.Context) 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])) + 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 ')'. - matches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables) - for _, match := range matches { - conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1])) + 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(s)) + conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, s)) } } conditionArray.FilterEmpty() @@ -141,8 +168,36 @@ func (m *Model) getConditionForSoftDeleting() string { 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)) + fieldName, fieldType := m.getSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) + if fieldName != "" { + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType) + } + return "" +} + +func (m *Model) 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 fieldType { + case LocalTypeDate, LocalTypeDatetime: + return fmt.Sprintf(`%s IS NULL`, quotedFieldName) + case LocalTypeInt, LocalTypeUint, LocalTypeInt64: + return fmt.Sprintf(`%s=0`, quotedFieldName) + case LocalTypeBool: + return fmt.Sprintf(`%s=0`, quotedFieldName) + default: + intlog.Errorf( + ctx, + `invalid field type "%s" of field name "%s" for soft deleting condition`, + fieldType, + ) } return "" } @@ -153,9 +208,8 @@ func (m *Model) getConditionForSoftDeleting() string { // - `test`.`demo` b // - `demo` // - demo -func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string { +func (m *Model) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string { var ( - field string table string schema string array1 = gstr.SplitAndTrim(s, " ") @@ -167,15 +221,48 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string { } else { table = array2[0] } - field = m.getSoftFieldNameDeleted(schema, table) - if field == "" { + fieldName, fieldType := m.getSoftFieldNameAndTypeDeleted(ctx, schema, table) + if fieldName == "" { return "" } if len(array1) >= 3 { - return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[2]), m.db.GetCore().QuoteWord(field)) + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType) } if len(array1) >= 2 { - return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[1]), m.db.GetCore().QuoteWord(field)) + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType) + } + return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType) +} + +func (m *Model) getDataByFieldNameAndTypeForSoftDeleting( + 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) } - return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(table), m.db.GetCore().QuoteWord(field)) + dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName) + dataValue = m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldType) + return +} + +func (m *Model) getValueByFieldTypeForCreateOrUpdate(ctx context.Context, fieldType LocalType) (dataValue any) { + switch fieldType { + case LocalTypeDate, LocalTypeDatetime: + dataValue = gtime.Now() + case LocalTypeInt, LocalTypeUint, LocalTypeInt64: + dataValue = gtime.Timestamp() + case LocalTypeBool: + dataValue = 1 + default: + intlog.Errorf( + ctx, + `invalid field type "%s" of field name "%s" for soft deleting data`, + fieldType, + ) + } + return } diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index 32c74967039..c3322cbd1cd 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -48,7 +48,7 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro var ( updateData = m.data reflectInfo = reflection.OriginTypeAndKind(updateData) - fieldNameUpdate = m.getSoftFieldNameUpdated("", m.tablesInit) + fieldNameUpdate, fieldTypeUpdate = m.getSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra ) @@ -69,9 +69,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 := m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) if fieldNameUpdate != "" && !gstr.Contains(updates, fieldNameUpdate) { updates += fmt.Sprintf(`,%s=?`, fieldNameUpdate) - conditionArgs = append([]interface{}{gtime.Now()}, conditionArgs...) + conditionArgs = append([]interface{}{dataValue}, conditionArgs...) } } updateData = updates From 5212868bf829e58692944b9de631f2cabc33b578 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 30 Jan 2024 22:20:50 +0800 Subject: [PATCH 2/7] up --- database/gdb/gdb_model_delete.go | 8 +++- database/gdb/gdb_model_insert.go | 8 ++-- database/gdb/gdb_model_select.go | 2 +- database/gdb/gdb_model_time.go | 74 ++++++++++++++++++++++++-------- database/gdb/gdb_model_update.go | 6 ++- 5 files changed, 72 insertions(+), 26 deletions(-) diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index ab010cb84f5..fa7bc770c72 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -29,9 +29,11 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { } }() var ( - fieldNameDelete, fieldTypeDelete = m.getSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra + fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetSoftFieldNameAndTypeDeleted( + ctx, "", m.tablesInit, + ) ) if m.unscoped { fieldNameDelete = "" @@ -50,7 +52,9 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { // Soft deleting. if fieldNameDelete != "" { - dataHolder, dataValue := m.getDataByFieldNameAndTypeForSoftDeleting(ctx, "", fieldNameDelete, fieldTypeDelete) + dataHolder, dataValue := m.softTimeMaintainer().GetDataByFieldNameAndTypeForSoftDeleting( + ctx, "", fieldNameDelete, fieldTypeDelete, + ) in := &HookUpdateInput{ internalParamHookUpdate: internalParamHookUpdate{ internalParamHook: internalParamHook{ diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index 5f67685bdb7..afe19ea2f2c 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -243,8 +243,8 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio } var ( list List - fieldNameCreate, fieldTypeCreate = m.getSoftFieldNameAndTypeCreated(ctx, "", m.tablesInit) - fieldNameUpdate, fieldTypeUpdate = m.getSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) + fieldNameCreate, fieldTypeCreate = m.softTimeMaintainer().GetSoftFieldNameAndTypeCreated(ctx, "", m.tablesInit) + fieldNameUpdate, fieldTypeUpdate = m.softTimeMaintainer().GetSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) ) newData, err := m.filterDataForInsertOrUpdate(m.data) if err != nil { @@ -307,10 +307,10 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio if !m.unscoped && (fieldNameCreate != "" || fieldNameUpdate != "") { for k, v := range list { if fieldNameCreate != "" { - v[fieldNameCreate] = m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate) + v[fieldNameCreate] = m.softTimeMaintainer().GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate) } if fieldNameUpdate != "" { - v[fieldNameUpdate] = m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) + v[fieldNameUpdate] = m.softTimeMaintainer().GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) } list[k] = v } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index f90a86d7c1e..47027a57f8d 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(ctx) + softDeletingCondition := m.softTimeMaintainer().GetConditionForSoftDeleting(ctx) if m.rawSql != "" && conditionWhere != "" { if gstr.ContainsI(m.rawSql, " WHERE ") { conditionWhere = " AND " + conditionWhere diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go index 3381285d9d2..7cef0e43660 100644 --- a/database/gdb/gdb_model_time.go +++ b/database/gdb/gdb_model_time.go @@ -19,6 +19,34 @@ import ( "github.com/gogf/gf/v2/util/gutil" ) +type softTimeMaintainer struct { + *Model +} + +type iSoftTimeMaintainer interface { + GetSoftFieldNameAndTypeCreated( + ctx context.Context, schema string, table string, + ) (fieldName string, fieldType LocalType) + + GetSoftFieldNameAndTypeUpdated( + ctx context.Context, schema string, table string, + ) (fieldName string, fieldType LocalType) + + GetSoftFieldNameAndTypeDeleted( + ctx context.Context, schema string, table string, + ) (fieldName string, fieldType LocalType) + + GetValueByFieldTypeForCreateOrUpdate( + ctx context.Context, fieldType LocalType, + ) (dataValue any) + + GetDataByFieldNameAndTypeForSoftDeleting( + ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, + ) (dataHolder string, dataValue any) + + GetConditionForSoftDeleting(ctx context.Context) string +} + 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. @@ -32,14 +60,20 @@ func (m *Model) Unscoped() *Model { return model } -// getSoftFieldNameAndTypeCreated checks and returns the field name for record creating time. +func (m *Model) softTimeMaintainer() iSoftTimeMaintainer { + return &softTimeMaintainer{ + m, + } +} + +// GetSoftFieldNameAndTypeCreated 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) getSoftFieldNameAndTypeCreated( +func (m *softTimeMaintainer) GetSoftFieldNameAndTypeCreated( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. - if m.db.GetConfig().TimeMaintainDisabled { + if m.Model.db.GetConfig().TimeMaintainDisabled { return "", LocalTypeUndefined } tableName := "" @@ -59,10 +93,10 @@ func (m *Model) getSoftFieldNameAndTypeCreated( ) } -// getSoftFieldNameAndTypeUpdated checks and returns the field name for record updating time. +// GetSoftFieldNameAndTypeUpdated 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) getSoftFieldNameAndTypeUpdated( +func (m *softTimeMaintainer) GetSoftFieldNameAndTypeUpdated( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. @@ -86,10 +120,10 @@ func (m *Model) getSoftFieldNameAndTypeUpdated( ) } -// getSoftFieldNameAndTypeDeleted checks and returns the field name for record deleting time. +// GetSoftFieldNameAndTypeDeleted 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) getSoftFieldNameAndTypeDeleted( +func (m *softTimeMaintainer) GetSoftFieldNameAndTypeDeleted( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. @@ -114,7 +148,7 @@ func (m *Model) getSoftFieldNameAndTypeDeleted( } // getSoftFieldName retrieves and returns the field name of the table for possible key. -func (m *Model) getSoftFieldNameAndType( +func (m *softTimeMaintainer) getSoftFieldNameAndType( ctx context.Context, schema string, table string, checkFiledNames []string, ) (fieldName string, fieldType LocalType) { @@ -136,13 +170,13 @@ func (m *Model) getSoftFieldNameAndType( return } -// getConditionForSoftDeleting retrieves and returns the condition string for soft deleting. +// 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(ctx context.Context) string { +func (m *softTimeMaintainer) GetConditionForSoftDeleting(ctx context.Context) string { if m.unscoped { return "" } @@ -168,14 +202,14 @@ func (m *Model) getConditionForSoftDeleting(ctx context.Context) string { return conditionArray.Join(" AND ") } // Only one table. - fieldName, fieldType := m.getSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) + fieldName, fieldType := m.GetSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) if fieldName != "" { return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType) } return "" } -func (m *Model) getConditionByFieldNameAndTypeForSoftDeleting( +func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting( ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, ) string { var ( @@ -208,7 +242,7 @@ func (m *Model) getConditionByFieldNameAndTypeForSoftDeleting( // - `test`.`demo` b // - `demo` // - demo -func (m *Model) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string { +func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string { var ( table string schema string @@ -221,7 +255,7 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(ctx context.Context, s } else { table = array2[0] } - fieldName, fieldType := m.getSoftFieldNameAndTypeDeleted(ctx, schema, table) + fieldName, fieldType := m.GetSoftFieldNameAndTypeDeleted(ctx, schema, table) if fieldName == "" { return "" } @@ -234,7 +268,9 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(ctx context.Context, s return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType) } -func (m *Model) getDataByFieldNameAndTypeForSoftDeleting( +// GetDataByFieldNameAndTypeForSoftDeleting creates and returns the placeholder and value for +// specified field name and type in soft-deleting scenario. +func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForSoftDeleting( ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, ) (dataHolder string, dataValue any) { var ( @@ -245,11 +281,15 @@ func (m *Model) getDataByFieldNameAndTypeForSoftDeleting( quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName) } dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName) - dataValue = m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldType) + dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType) return } -func (m *Model) getValueByFieldTypeForCreateOrUpdate(ctx context.Context, fieldType LocalType) (dataValue any) { +// 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, +) (dataValue any) { switch fieldType { case LocalTypeDate, LocalTypeDatetime: dataValue = gtime.Now() diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index c3322cbd1cd..88e959e21ee 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -48,9 +48,11 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro var ( updateData = m.data reflectInfo = reflection.OriginTypeAndKind(updateData) - fieldNameUpdate, fieldTypeUpdate = m.getSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra + fieldNameUpdate, fieldTypeUpdate = m.softTimeMaintainer().GetSoftFieldNameAndTypeUpdated( + ctx, "", m.tablesInit, + ) ) if m.unscoped { fieldNameUpdate = "" @@ -69,7 +71,7 @@ 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 := m.getValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) + dataValue := m.softTimeMaintainer().GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) if fieldNameUpdate != "" && !gstr.Contains(updates, fieldNameUpdate) { updates += fmt.Sprintf(`,%s=?`, fieldNameUpdate) conditionArgs = append([]interface{}{dataValue}, conditionArgs...) From 55023f8ff9f8ca3a0efb672e8ac6fed68f51548b Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 31 Jan 2024 14:31:32 +0800 Subject: [PATCH 3/7] add field type detection for soft time field like created_at/updated_at/deleted_at --- ...=> mysql_z_unit_feature_soft_time_test.go} | 181 ++++++++++++++++-- database/gdb/gdb_model_insert.go | 22 ++- ...b_model_time.go => gdb_model_soft_time.go} | 25 ++- database/gdb/gdb_model_update.go | 9 +- 4 files changed, 208 insertions(+), 29 deletions(-) rename contrib/drivers/mysql/{mysql_z_unit_feature_time_maintain_test.go => mysql_z_unit_feature_soft_time_test.go} (84%) rename database/gdb/{gdb_model_time.go => gdb_model_soft_time.go} (96%) 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 84% 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 13aac68b466..a6fa5423160 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 @@ -17,8 +17,8 @@ import ( ) // 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 +151,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 +285,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 +419,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 +560,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 +600,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 +657,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 +796,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 +838,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 +989,158 @@ 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(one["create_at"].Int64(), one["update_at"].Int64()) + }) + + // 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.AssertNE(one["create_at"].Int64(), one["update_at"].Int64()) + + 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) + + // 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(one["create_at"].Int64(), one["update_at"].Int64()) + }) + + // 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) + }) +} diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index afe19ea2f2c..65802d79b3c 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -243,8 +243,10 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio } var ( list List - fieldNameCreate, fieldTypeCreate = m.softTimeMaintainer().GetSoftFieldNameAndTypeCreated(ctx, "", m.tablesInit) - fieldNameUpdate, fieldTypeUpdate = m.softTimeMaintainer().GetSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) + stm = m.softTimeMaintainer() + fieldNameCreate, fieldTypeCreate = stm.GetSoftFieldNameAndTypeCreated(ctx, "", m.tablesInit) + fieldNameUpdate, fieldTypeUpdate = stm.GetSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) + fieldNameDelete, fieldTypeDelete = stm.GetSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) ) newData, err := m.filterDataForInsertOrUpdate(m.data) if err != nil { @@ -307,10 +309,22 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio if !m.unscoped && (fieldNameCreate != "" || fieldNameUpdate != "") { for k, v := range list { if fieldNameCreate != "" { - v[fieldNameCreate] = m.softTimeMaintainer().GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate) + fieldCreateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate, false) + if fieldCreateValue != nil { + v[fieldNameCreate] = fieldCreateValue + } } if fieldNameUpdate != "" { - v[fieldNameUpdate] = m.softTimeMaintainer().GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) + 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_time.go b/database/gdb/gdb_model_soft_time.go similarity index 96% rename from database/gdb/gdb_model_time.go rename to database/gdb/gdb_model_soft_time.go index 7cef0e43660..f6f279008ef 100644 --- a/database/gdb/gdb_model_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -37,7 +37,7 @@ type iSoftTimeMaintainer interface { ) (fieldName string, fieldType LocalType) GetValueByFieldTypeForCreateOrUpdate( - ctx context.Context, fieldType LocalType, + ctx context.Context, fieldType LocalType, isDeletedField bool, ) (dataValue any) GetDataByFieldNameAndTypeForSoftDeleting( @@ -281,22 +281,31 @@ func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForSoftDeleting( quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName) } dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName) - dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType) + dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false) 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, -) (dataValue any) { + ctx context.Context, fieldType LocalType, isDeletedField bool, +) any { switch fieldType { case LocalTypeDate, LocalTypeDatetime: - dataValue = gtime.Now() + if isDeletedField { + return nil + } + return gtime.Now() case LocalTypeInt, LocalTypeUint, LocalTypeInt64: - dataValue = gtime.Timestamp() + if isDeletedField { + return 0 + } + return gtime.Timestamp() case LocalTypeBool: - dataValue = 1 + if isDeletedField { + return 0 + } + return 1 default: intlog.Errorf( ctx, @@ -304,5 +313,5 @@ func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate( fieldType, ) } - return + return nil } diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index 88e959e21ee..f621c9748b4 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,12 @@ 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) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra - fieldNameUpdate, fieldTypeUpdate = m.softTimeMaintainer().GetSoftFieldNameAndTypeUpdated( + fieldNameUpdate, fieldTypeUpdate = stm.GetSoftFieldNameAndTypeUpdated( ctx, "", m.tablesInit, ) ) @@ -63,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 @@ -71,7 +72,7 @@ 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 := m.softTimeMaintainer().GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate) + dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false) if fieldNameUpdate != "" && !gstr.Contains(updates, fieldNameUpdate) { updates += fmt.Sprintf(`,%s=?`, fieldNameUpdate) conditionArgs = append([]interface{}{dataValue}, conditionArgs...) From 529fed06198336be5b61e9a246f495a7c7c30f9b Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 31 Jan 2024 17:06:03 +0800 Subject: [PATCH 4/7] up --- database/gdb/gdb_model_soft_time.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/gdb/gdb_model_soft_time.go b/database/gdb/gdb_model_soft_time.go index f6f279008ef..7b55fba11b4 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -229,8 +229,8 @@ func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting( default: intlog.Errorf( ctx, - `invalid field type "%s" of field name "%s" for soft deleting condition`, - fieldType, + `invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`, + fieldType, fieldName, fieldPrefix, ) } return "" @@ -309,7 +309,7 @@ func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate( default: intlog.Errorf( ctx, - `invalid field type "%s" of field name "%s" for soft deleting data`, + `invalid field type "%s" for soft deleting data`, fieldType, ) } From b1d1ec7994534721e84bedc2f329ee0650f2e1f8 Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 31 Jan 2024 17:11:39 +0800 Subject: [PATCH 5/7] up --- database/gdb/gdb_model_soft_time.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/gdb/gdb_model_soft_time.go b/database/gdb/gdb_model_soft_time.go index 7b55fba11b4..5b553ad9619 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -73,7 +73,7 @@ func (m *softTimeMaintainer) GetSoftFieldNameAndTypeCreated( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. - if m.Model.db.GetConfig().TimeMaintainDisabled { + if m.db.GetConfig().TimeMaintainDisabled { return "", LocalTypeUndefined } tableName := "" From 95f470aff94d530f06eba0332d359dbd7ffd6d14 Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 31 Jan 2024 17:41:12 +0800 Subject: [PATCH 6/7] up --- .../mysql_z_unit_feature_soft_time_test.go | 1 + database/gdb/gdb_model_delete.go | 4 +- database/gdb/gdb_model_insert.go | 6 +- database/gdb/gdb_model_select.go | 2 +- database/gdb/gdb_model_soft_time.go | 94 ++++++++++++------- database/gdb/gdb_model_update.go | 2 +- os/gcache/gcache.go | 2 +- 7 files changed, 71 insertions(+), 40 deletions(-) diff --git a/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go index a6fa5423160..5996819dbfa 100644 --- a/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go @@ -1105,6 +1105,7 @@ CREATE TABLE %s ( } defer dropTable(table) + //db.SetDebug(true) // insert gtest.C(t, func(t *gtest.T) { dataInsert := g.Map{ diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index fa7bc770c72..b6a32c52424 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -31,7 +31,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { var ( conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra - fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetSoftFieldNameAndTypeDeleted( + fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetFieldNameAndTypeForDelete( ctx, "", m.tablesInit, ) ) @@ -52,7 +52,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { // Soft deleting. if fieldNameDelete != "" { - dataHolder, dataValue := m.softTimeMaintainer().GetDataByFieldNameAndTypeForSoftDeleting( + dataHolder, dataValue := m.softTimeMaintainer().GetDataByFieldNameAndTypeForDelete( ctx, "", fieldNameDelete, fieldTypeDelete, ) in := &HookUpdateInput{ diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index 65802d79b3c..c8f53489945 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -244,9 +244,9 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio var ( list List stm = m.softTimeMaintainer() - fieldNameCreate, fieldTypeCreate = stm.GetSoftFieldNameAndTypeCreated(ctx, "", m.tablesInit) - fieldNameUpdate, fieldTypeUpdate = stm.GetSoftFieldNameAndTypeUpdated(ctx, "", m.tablesInit) - fieldNameDelete, fieldTypeDelete = stm.GetSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) + fieldNameCreate, fieldTypeCreate = stm.GetFieldNameAndTypeForCreate(ctx, "", m.tablesInit) + fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate(ctx, "", m.tablesInit) + fieldNameDelete, fieldTypeDelete = stm.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit) ) newData, err := m.filterDataForInsertOrUpdate(m.data) if err != nil { diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 47027a57f8d..69bfd38c399 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.softTimeMaintainer().GetConditionForSoftDeleting(ctx) + 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 index 5b553ad9619..cf1e1bdceef 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -12,6 +12,7 @@ import ( "github.com/gogf/gf/v2/container/garray" "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" @@ -24,15 +25,15 @@ type softTimeMaintainer struct { } type iSoftTimeMaintainer interface { - GetSoftFieldNameAndTypeCreated( + GetFieldNameAndTypeForCreate( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) - GetSoftFieldNameAndTypeUpdated( + GetFieldNameAndTypeForUpdate( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) - GetSoftFieldNameAndTypeDeleted( + GetFieldNameAndTypeForDelete( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) @@ -40,17 +41,26 @@ type iSoftTimeMaintainer interface { ctx context.Context, fieldType LocalType, isDeletedField bool, ) (dataValue any) - GetDataByFieldNameAndTypeForSoftDeleting( + GetDataByFieldNameAndTypeForDelete( ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, ) (dataHolder string, dataValue any) - GetConditionForSoftDeleting(ctx context.Context) string + GetWhereConditionForDelete(ctx context.Context) string +} + +// getSoftFieldNameAndTypeCacheItem is the internal struct for storing create/update/delete fields. +type getSoftFieldNameAndTypeCacheItem struct { + FieldName string + FieldType LocalType } 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. + // 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"} ) // Unscoped disables the auto-update time feature for insert, update and delete options. @@ -66,10 +76,10 @@ func (m *Model) softTimeMaintainer() iSoftTimeMaintainer { } } -// GetSoftFieldNameAndTypeCreated checks and returns the field name for record creating time. +// 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) GetSoftFieldNameAndTypeCreated( +func (m *softTimeMaintainer) GetFieldNameAndTypeForCreate( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. @@ -93,10 +103,10 @@ func (m *softTimeMaintainer) GetSoftFieldNameAndTypeCreated( ) } -// GetSoftFieldNameAndTypeUpdated checks and returns the field name for record updating time. +// 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) GetSoftFieldNameAndTypeUpdated( +func (m *softTimeMaintainer) GetFieldNameAndTypeForUpdate( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. @@ -120,10 +130,10 @@ func (m *softTimeMaintainer) GetSoftFieldNameAndTypeUpdated( ) } -// GetSoftFieldNameAndTypeDeleted checks and returns the field name for record deleting time. +// 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) GetSoftFieldNameAndTypeDeleted( +func (m *softTimeMaintainer) GetFieldNameAndTypeForDelete( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) { // It checks whether this feature disabled. @@ -152,31 +162,51 @@ func (m *softTimeMaintainer) getSoftFieldNameAndType( ctx context.Context, schema string, table string, checkFiledNames []string, ) (fieldName string, fieldType LocalType) { - // 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, - ) - return + 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 } -// GetConditionForSoftDeleting retrieves and returns the condition string for soft deleting. +// 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) GetConditionForSoftDeleting(ctx context.Context) string { +func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) string { if m.unscoped { return "" } @@ -202,7 +232,7 @@ func (m *softTimeMaintainer) GetConditionForSoftDeleting(ctx context.Context) st return conditionArray.Join(" AND ") } // Only one table. - fieldName, fieldType := m.GetSoftFieldNameAndTypeDeleted(ctx, "", m.tablesInit) + fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit) if fieldName != "" { return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType) } @@ -255,7 +285,7 @@ func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx contex } else { table = array2[0] } - fieldName, fieldType := m.GetSoftFieldNameAndTypeDeleted(ctx, schema, table) + fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table) if fieldName == "" { return "" } @@ -268,9 +298,9 @@ func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx contex return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType) } -// GetDataByFieldNameAndTypeForSoftDeleting creates and returns the placeholder and value for +// GetDataByFieldNameAndTypeForDelete creates and returns the placeholder and value for // specified field name and type in soft-deleting scenario. -func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForSoftDeleting( +func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete( ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType, ) (dataHolder string, dataValue any) { var ( diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index f621c9748b4..4f2f11cb374 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -50,7 +50,7 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro reflectInfo = reflection.OriginTypeAndKind(updateData) conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false) conditionStr = conditionWhere + conditionExtra - fieldNameUpdate, fieldTypeUpdate = stm.GetSoftFieldNameAndTypeUpdated( + fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate( ctx, "", m.tablesInit, ) ) diff --git a/os/gcache/gcache.go b/os/gcache/gcache.go index 749218a1e72..645508b786d 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. From 9aaa047ea59bd451c9226128837eb3bd29a4fb4a Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 31 Jan 2024 21:42:02 +0800 Subject: [PATCH 7/7] add SoftTimeOption for gdb.Model --- .../mysql_z_unit_feature_soft_time_test.go | 128 ++++++++++++- database/gdb/gdb_model.go | 69 +++---- database/gdb/gdb_model_soft_time.go | 170 +++++++++++++----- 3 files changed, 283 insertions(+), 84 deletions(-) diff --git a/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go index 5996819dbfa..10f12918bcd 100644 --- a/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_feature_soft_time_test.go @@ -11,6 +11,7 @@ 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" @@ -1023,7 +1024,8 @@ CREATE TABLE %s ( t.AssertGT(one["create_at"].Int64(), 0) t.AssertGT(one["update_at"].Int64(), 0) t.Assert(one["delete_at"].Int64(), 0) - t.Assert(one["create_at"].Int64(), one["update_at"].Int64()) + 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. @@ -1046,7 +1048,8 @@ CREATE TABLE %s ( t.AssertGT(one["create_at"].Int64(), 0) t.AssertGT(one["update_at"].Int64(), 0) t.Assert(one["delete_at"].Int64(), 0) - t.AssertNE(one["create_at"].Int64(), one["update_at"].Int64()) + t.Assert(len(one["create_at"].String()), 10) + t.Assert(len(one["update_at"].String()), 10) var ( lastCreateTime = one["create_at"].Int64() @@ -1123,7 +1126,8 @@ CREATE TABLE %s ( t.AssertGT(one["create_at"].Int64(), 0) t.AssertGT(one["update_at"].Int64(), 0) t.Assert(one["delete_at"].Int64(), 0) - t.Assert(one["create_at"].Int64(), one["update_at"].Int64()) + t.Assert(len(one["create_at"].String()), 10) + t.Assert(len(one["update_at"].String()), 10) }) // delete @@ -1145,3 +1149,121 @@ CREATE TABLE %s ( 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_model.go b/database/gdb/gdb_model.go index dc8f596bac7..f854063640f 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_soft_time.go b/database/gdb/gdb_model_soft_time.go index cf1e1bdceef..e9ac66a0974 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -11,6 +11,8 @@ import ( "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" @@ -20,6 +22,23 @@ import ( "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 } @@ -63,7 +82,14 @@ var ( deletedFieldNames = []string{"deleted_at", "delete_at"} ) -// Unscoped disables the auto-update time feature for insert, update and delete options. +// 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 @@ -239,33 +265,6 @@ func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) str 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 fieldType { - case LocalTypeDate, LocalTypeDatetime: - return fmt.Sprintf(`%s IS NULL`, quotedFieldName) - case LocalTypeInt, LocalTypeUint, LocalTypeInt64: - return fmt.Sprintf(`%s=0`, quotedFieldName) - case 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, - ) - } - return "" -} - // getConditionOfTableStringForSoftDeleting does something as its name describes. // Examples for `s`: // - `test`.`demo` as b @@ -315,33 +314,110 @@ func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete( 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 { - switch fieldType { - case LocalTypeDate, LocalTypeDatetime: - if isDeletedField { - return nil + 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, + ) } - return gtime.Now() - case LocalTypeInt, LocalTypeUint, LocalTypeInt64: - if isDeletedField { - return 0 + + default: + switch fieldType { + case LocalTypeBool: + value = 1 + default: + value = m.createValueBySoftTimeOption(isDeletedField) } - return gtime.Timestamp() - case LocalTypeBool: - if isDeletedField { - return 0 + } + 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 1 + 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: - intlog.Errorf( - ctx, - `invalid field type "%s" for soft deleting data`, - fieldType, - ) + panic(gerror.NewCodef( + gcode.CodeInternalPanic, + `unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType, + )) } - return nil + return value }