mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
本次PR主要针对GoFrame ORM中的软删除`SoftTime`功能进行了优化 - 新增`SoftTimeFieldType`枚举类型,用于区分创建、更新、删除三种不同的软时间字段 - 替代之前使用的魔数方式,提高类型安全性 - 将原有的6个方法精简为4个方法, 合并了三个几乎相同的`GetFieldNameAndTypeFor*`方法为统一的方法 This pull request refactors and simplifies the "soft time" (created/updated/deleted timestamp) handling logic in the database layer, making the codebase more maintainable and extensible. The changes consolidate multiple similar methods into general-purpose ones, improve cache key generation, and clarify the logic for generating and applying soft time field values and conditions. Key changes include: **Soft Time API Refactoring and Simplification** - Consolidated multiple methods (`GetFieldNameAndTypeForCreate`, `GetFieldNameAndTypeForUpdate`, `GetFieldNameAndTypeForDelete`) into a single, parameterized method `GetFieldInfo`, reducing code duplication and making it easier to support new soft time field types. The interface and implementation for soft time maintenance (`iSoftTimeMaintainer`, `softTimeMaintainer`) have been updated accordingly. [[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L46-R66) [[2]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L105-R180) - Combined and renamed methods for generating soft time field values and delete conditions, such as merging `GetValueByFieldTypeForCreateOrUpdate` into `GetFieldValue`, and `GetWhereConditionForDelete` into `GetDeleteCondition`. Related usages throughout the codebase have been updated to use the new methods. [[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L255-R206) [[2]](diffhunk://#diff-97beb485550e4381182a04bbb857a25b7f4ecd4a594dff8ac884cfaae38f3046L34-R35) [[3]](diffhunk://#diff-97beb485550e4381182a04bbb857a25b7f4ecd4a594dff8ac884cfaae38f3046L55-R55) [[4]](diffhunk://#diff-88304ddb7791aedbd83dafb68374aecab286d1356a7f2f149a8e57ac1a7f40b4L265-R267) [[5]](diffhunk://#diff-88304ddb7791aedbd83dafb68374aecab286d1356a7f2f149a8e57ac1a7f40b4L298-R311) [[6]](diffhunk://#diff-d4f6e0370e049dea52f3db9a13c64e2cfb2f7ef012433186e21179149b626d0fL944-R944) **Soft Delete Logic Improvements** - Refactored the logic for building soft delete WHERE conditions and generating update data, with clearer and more robust handling of field types and prefixes. Introduced helper methods like `buildDeleteCondition`, `GetDeleteData`, and improved error logging for invalid field types. [[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L287-R234) [[2]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L313-R395) **Cache Key Generation Enhancements** - Added dedicated helper functions for generating cache keys for table names, table fields, select queries, and soft time field/type lookups, improving cache consistency and code readability. [[1]](diffhunk://#diff-d57d57e6f9b342ba6fa30c4bb413e2f4f3514a8cd5ad36949eef126e5f8b7ac9R969) [[2]](diffhunk://#diff-d57d57e6f9b342ba6fa30c4bb413e2f4f3514a8cd5ad36949eef126e5f8b7ac9R980) [[3]](diffhunk://#diff-d57d57e6f9b342ba6fa30c4bb413e2f4f3514a8cd5ad36949eef126e5f8b7ac9R993-R1002) [[4]](diffhunk://#diff-b1bbe5e3995261813e4e0ac6ffee8a37c236eaa2759f2bd82e211711695a70bcL790-R790) **General Code Cleanup** - Removed redundant code, clarified comments, and improved naming throughout the affected files, making the code easier to follow and maintain. [[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L105-R180) [[2]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L255-R206) Let me know if you'd like a walkthrough of any specific part of the refactored soft time logic!
385 lines
12 KiB
Go
385 lines
12 KiB
Go
// 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"
|
|
"strings"
|
|
|
|
"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/internal/utils"
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// SoftTimeFieldType represents different soft time field purposes.
|
|
type SoftTimeFieldType int
|
|
|
|
const (
|
|
SoftTimeFieldCreate SoftTimeFieldType = iota
|
|
SoftTimeFieldUpdate
|
|
SoftTimeFieldDelete
|
|
)
|
|
|
|
type iSoftTimeMaintainer interface {
|
|
// GetFieldInfo returns field name and type for specified field purpose.
|
|
GetFieldInfo(ctx context.Context, schema, table string, fieldPurpose SoftTimeFieldType) (fieldName string, localType LocalType)
|
|
|
|
// GetFieldValue generates value for create/update/delete operations.
|
|
GetFieldValue(ctx context.Context, localType LocalType, isDeleted bool) any
|
|
|
|
// GetDeleteCondition returns WHERE condition for soft delete query.
|
|
GetDeleteCondition(ctx context.Context) string
|
|
|
|
// GetDeleteData returns UPDATE statement data for soft delete.
|
|
GetDeleteData(ctx context.Context, prefix, fieldName string, localType LocalType) (holder string, value any)
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// GetFieldInfo returns field name and type for specified field purpose.
|
|
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
|
|
func (m *softTimeMaintainer) GetFieldInfo(
|
|
ctx context.Context, schema, table string, fieldPurpose SoftTimeFieldType,
|
|
) (fieldName string, localType LocalType) {
|
|
// Check if feature is disabled
|
|
if m.db.GetConfig().TimeMaintainDisabled {
|
|
return "", LocalTypeUndefined
|
|
}
|
|
|
|
// Determine table name
|
|
tableName := table
|
|
if tableName == "" {
|
|
tableName = m.tablesInit
|
|
}
|
|
|
|
// Get config and field candidates
|
|
config := m.db.GetConfig()
|
|
var (
|
|
configField string
|
|
defaultFields []string
|
|
)
|
|
|
|
switch fieldPurpose {
|
|
case SoftTimeFieldCreate:
|
|
configField = config.CreatedAt
|
|
defaultFields = createdFieldNames
|
|
case SoftTimeFieldUpdate:
|
|
configField = config.UpdatedAt
|
|
defaultFields = updatedFieldNames
|
|
case SoftTimeFieldDelete:
|
|
configField = config.DeletedAt
|
|
defaultFields = deletedFieldNames
|
|
}
|
|
|
|
// Use config field if specified, otherwise use defaults
|
|
if configField != "" {
|
|
return m.getSoftFieldNameAndType(ctx, schema, tableName, []string{configField})
|
|
}
|
|
return m.getSoftFieldNameAndType(ctx, schema, tableName, defaultFields)
|
|
}
|
|
|
|
// getSoftFieldNameAndType retrieves and returns the field name of the table for possible key.
|
|
func (m *softTimeMaintainer) getSoftFieldNameAndType(
|
|
ctx context.Context, schema, table string, candidateFields []string,
|
|
) (fieldName string, fieldType LocalType) {
|
|
// Build cache key
|
|
cacheKey := genSoftTimeFieldNameTypeCacheKey(schema, table, candidateFields)
|
|
|
|
// Try to get from cache
|
|
cache := m.db.GetCore().GetInnerMemCache()
|
|
result, err := cache.GetOrSetFunc(ctx, cacheKey, func(ctx context.Context) (any, error) {
|
|
// Get table fields
|
|
fieldsMap, err := m.TableFields(table, schema)
|
|
if err != nil || len(fieldsMap) == 0 {
|
|
return nil, err
|
|
}
|
|
|
|
// Search for matching field
|
|
for _, field := range candidateFields {
|
|
if name := searchFieldNameFromMap(fieldsMap, field); name != "" {
|
|
fType, _ := m.db.CheckLocalTypeForField(ctx, fieldsMap[name].Type, nil)
|
|
return getSoftFieldNameAndTypeCacheItem{
|
|
FieldName: name,
|
|
FieldType: fType,
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}, gcache.DurationNoExpire)
|
|
|
|
if err != nil || result == nil {
|
|
return "", LocalTypeUndefined
|
|
}
|
|
|
|
item := result.Val().(getSoftFieldNameAndTypeCacheItem)
|
|
return item.FieldName, item.FieldType
|
|
}
|
|
|
|
func searchFieldNameFromMap(fieldsMap map[string]*TableField, key string) string {
|
|
if len(fieldsMap) == 0 {
|
|
return ""
|
|
}
|
|
_, ok := fieldsMap[key]
|
|
if ok {
|
|
return key
|
|
}
|
|
key = utils.RemoveSymbols(key)
|
|
for k := range fieldsMap {
|
|
if strings.EqualFold(utils.RemoveSymbols(k), key) {
|
|
return k
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetDeleteCondition returns WHERE condition for soft delete query.
|
|
// 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) GetDeleteCondition(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.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldDelete)
|
|
if fieldName != "" {
|
|
return m.buildDeleteCondition(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.GetFieldInfo(ctx, schema, table, SoftTimeFieldDelete)
|
|
if fieldName == "" {
|
|
return ""
|
|
}
|
|
if len(array1) >= 3 {
|
|
return m.buildDeleteCondition(ctx, array1[2], fieldName, fieldType)
|
|
}
|
|
if len(array1) >= 2 {
|
|
return m.buildDeleteCondition(ctx, array1[1], fieldName, fieldType)
|
|
}
|
|
return m.buildDeleteCondition(ctx, table, fieldName, fieldType)
|
|
}
|
|
|
|
// GetDeleteData returns UPDATE statement data for soft delete.
|
|
func (m *softTimeMaintainer) GetDeleteData(
|
|
ctx context.Context, prefix, fieldName string, fieldType LocalType,
|
|
) (holder string, value any) {
|
|
core := m.db.GetCore()
|
|
quotedName := core.QuoteWord(fieldName)
|
|
|
|
if prefix != "" {
|
|
quotedName = fmt.Sprintf(`%s.%s`, core.QuoteWord(prefix), quotedName)
|
|
}
|
|
|
|
holder = fmt.Sprintf(`%s=?`, quotedName)
|
|
value = m.GetFieldValue(ctx, fieldType, false)
|
|
return
|
|
}
|
|
|
|
// buildDeleteCondition builds WHERE condition for soft delete filtering.
|
|
func (m *softTimeMaintainer) buildDeleteCondition(
|
|
ctx context.Context, prefix, fieldName string, fieldType LocalType,
|
|
) string {
|
|
core := m.db.GetCore()
|
|
quotedName := core.QuoteWord(fieldName)
|
|
|
|
if prefix != "" {
|
|
quotedName = fmt.Sprintf(`%s.%s`, core.QuoteWord(prefix), quotedName)
|
|
}
|
|
switch m.softTimeOption.SoftTimeType {
|
|
case SoftTimeTypeAuto:
|
|
switch fieldType {
|
|
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
|
return fmt.Sprintf(`%s IS NULL`, quotedName)
|
|
case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64, LocalTypeBool:
|
|
return fmt.Sprintf(`%s=0`, quotedName)
|
|
default:
|
|
intlog.Errorf(ctx, `invalid field type "%s" for soft delete condition: prefix=%s, field=%s`, fieldType, prefix, fieldName)
|
|
return ""
|
|
}
|
|
|
|
case SoftTimeTypeTime:
|
|
return fmt.Sprintf(`%s IS NULL`, quotedName)
|
|
|
|
default:
|
|
return fmt.Sprintf(`%s=0`, quotedName)
|
|
}
|
|
}
|
|
|
|
// GetFieldValue generates value for create/update/delete operations.
|
|
func (m *softTimeMaintainer) GetFieldValue(
|
|
ctx context.Context, fieldType LocalType, isDeleted bool,
|
|
) any {
|
|
// For deleted field, return "empty" value
|
|
if isDeleted {
|
|
return m.getEmptyValue(fieldType)
|
|
}
|
|
|
|
// For create/update/delete, return current time value
|
|
switch m.softTimeOption.SoftTimeType {
|
|
case SoftTimeTypeAuto:
|
|
return m.getAutoValue(ctx, fieldType)
|
|
default:
|
|
switch fieldType {
|
|
case LocalTypeBool:
|
|
return 1
|
|
default:
|
|
return m.getTimestampValue()
|
|
}
|
|
}
|
|
}
|
|
|
|
// getTimestampValue returns timestamp value for soft time.
|
|
func (m *softTimeMaintainer) getTimestampValue() any {
|
|
switch m.softTimeOption.SoftTimeType {
|
|
case SoftTimeTypeTime:
|
|
return gtime.Now()
|
|
case SoftTimeTypeTimestamp:
|
|
return gtime.Timestamp()
|
|
case SoftTimeTypeTimestampMilli:
|
|
return gtime.TimestampMilli()
|
|
case SoftTimeTypeTimestampMicro:
|
|
return gtime.TimestampMicro()
|
|
case SoftTimeTypeTimestampNano:
|
|
return gtime.TimestampNano()
|
|
default:
|
|
panic(gerror.NewCodef(
|
|
gcode.CodeInternalPanic,
|
|
`unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType,
|
|
))
|
|
}
|
|
}
|
|
|
|
// getEmptyValue returns "empty" value for deleted field.
|
|
func (m *softTimeMaintainer) getEmptyValue(fieldType LocalType) any {
|
|
switch fieldType {
|
|
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
|
return nil
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// getAutoValue returns auto-detected value based on field type.
|
|
func (m *softTimeMaintainer) getAutoValue(ctx context.Context, fieldType LocalType) any {
|
|
switch fieldType {
|
|
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
|
return gtime.Now()
|
|
case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64:
|
|
return gtime.Timestamp()
|
|
case LocalTypeBool:
|
|
return 1
|
|
default:
|
|
intlog.Errorf(ctx, `invalid field type "%s" for soft time auto value`, fieldType)
|
|
return nil
|
|
}
|
|
}
|