github.com/gogf/gf/v2@v2.7.4/database/gdb/gdb_model_soft_time.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gdb
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"strings"
    13  
    14  	"github.com/gogf/gf/v2/container/garray"
    15  	"github.com/gogf/gf/v2/errors/gcode"
    16  	"github.com/gogf/gf/v2/errors/gerror"
    17  	"github.com/gogf/gf/v2/internal/intlog"
    18  	"github.com/gogf/gf/v2/internal/utils"
    19  	"github.com/gogf/gf/v2/os/gcache"
    20  	"github.com/gogf/gf/v2/os/gtime"
    21  	"github.com/gogf/gf/v2/text/gregex"
    22  	"github.com/gogf/gf/v2/text/gstr"
    23  )
    24  
    25  // SoftTimeType custom defines the soft time field type.
    26  type SoftTimeType int
    27  
    28  const (
    29  	SoftTimeTypeAuto           SoftTimeType = 0 // (Default)Auto detect the field type by table field type.
    30  	SoftTimeTypeTime           SoftTimeType = 1 // Using datetime as the field value.
    31  	SoftTimeTypeTimestamp      SoftTimeType = 2 // In unix seconds.
    32  	SoftTimeTypeTimestampMilli SoftTimeType = 3 // In unix milliseconds.
    33  	SoftTimeTypeTimestampMicro SoftTimeType = 4 // In unix microseconds.
    34  	SoftTimeTypeTimestampNano  SoftTimeType = 5 // In unix nanoseconds.
    35  )
    36  
    37  // SoftTimeOption is the option to customize soft time feature for Model.
    38  type SoftTimeOption struct {
    39  	SoftTimeType SoftTimeType // The value type for soft time field.
    40  }
    41  
    42  type softTimeMaintainer struct {
    43  	*Model
    44  }
    45  
    46  type iSoftTimeMaintainer interface {
    47  	GetFieldNameAndTypeForCreate(
    48  		ctx context.Context, schema string, table string,
    49  	) (fieldName string, fieldType LocalType)
    50  
    51  	GetFieldNameAndTypeForUpdate(
    52  		ctx context.Context, schema string, table string,
    53  	) (fieldName string, fieldType LocalType)
    54  
    55  	GetFieldNameAndTypeForDelete(
    56  		ctx context.Context, schema string, table string,
    57  	) (fieldName string, fieldType LocalType)
    58  
    59  	GetValueByFieldTypeForCreateOrUpdate(
    60  		ctx context.Context, fieldType LocalType, isDeletedField bool,
    61  	) (dataValue any)
    62  
    63  	GetDataByFieldNameAndTypeForDelete(
    64  		ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
    65  	) (dataHolder string, dataValue any)
    66  
    67  	GetWhereConditionForDelete(ctx context.Context) string
    68  }
    69  
    70  // getSoftFieldNameAndTypeCacheItem is the internal struct for storing create/update/delete fields.
    71  type getSoftFieldNameAndTypeCacheItem struct {
    72  	FieldName string
    73  	FieldType LocalType
    74  }
    75  
    76  var (
    77  	// Default field names of table for automatic-filled for record creating.
    78  	createdFieldNames = []string{"created_at", "create_at"}
    79  	// Default field names of table for automatic-filled for record updating.
    80  	updatedFieldNames = []string{"updated_at", "update_at"}
    81  	// Default field names of table for automatic-filled for record deleting.
    82  	deletedFieldNames = []string{"deleted_at", "delete_at"}
    83  )
    84  
    85  // SoftTime sets the SoftTimeOption to customize soft time feature for Model.
    86  func (m *Model) SoftTime(option SoftTimeOption) *Model {
    87  	model := m.getModel()
    88  	model.softTimeOption = option
    89  	return model
    90  }
    91  
    92  // Unscoped disables the soft time feature for insert, update and delete operations.
    93  func (m *Model) Unscoped() *Model {
    94  	model := m.getModel()
    95  	model.unscoped = true
    96  	return model
    97  }
    98  
    99  func (m *Model) softTimeMaintainer() iSoftTimeMaintainer {
   100  	return &softTimeMaintainer{
   101  		m,
   102  	}
   103  }
   104  
   105  // GetFieldNameAndTypeForCreate checks and returns the field name for record creating time.
   106  // If there's no field name for storing creating time, it returns an empty string.
   107  // It checks the key with or without cases or chars '-'/'_'/'.'/' '.
   108  func (m *softTimeMaintainer) GetFieldNameAndTypeForCreate(
   109  	ctx context.Context, schema string, table string,
   110  ) (fieldName string, fieldType LocalType) {
   111  	// It checks whether this feature disabled.
   112  	if m.db.GetConfig().TimeMaintainDisabled {
   113  		return "", LocalTypeUndefined
   114  	}
   115  	tableName := ""
   116  	if table != "" {
   117  		tableName = table
   118  	} else {
   119  		tableName = m.tablesInit
   120  	}
   121  	config := m.db.GetConfig()
   122  	if config.CreatedAt != "" {
   123  		return m.getSoftFieldNameAndType(
   124  			ctx, schema, tableName, []string{config.CreatedAt},
   125  		)
   126  	}
   127  	return m.getSoftFieldNameAndType(
   128  		ctx, schema, tableName, createdFieldNames,
   129  	)
   130  }
   131  
   132  // GetFieldNameAndTypeForUpdate checks and returns the field name for record updating time.
   133  // If there's no field name for storing updating time, it returns an empty string.
   134  // It checks the key with or without cases or chars '-'/'_'/'.'/' '.
   135  func (m *softTimeMaintainer) GetFieldNameAndTypeForUpdate(
   136  	ctx context.Context, schema string, table string,
   137  ) (fieldName string, fieldType LocalType) {
   138  	// It checks whether this feature disabled.
   139  	if m.db.GetConfig().TimeMaintainDisabled {
   140  		return "", LocalTypeUndefined
   141  	}
   142  	tableName := ""
   143  	if table != "" {
   144  		tableName = table
   145  	} else {
   146  		tableName = m.tablesInit
   147  	}
   148  	config := m.db.GetConfig()
   149  	if config.UpdatedAt != "" {
   150  		return m.getSoftFieldNameAndType(
   151  			ctx, schema, tableName, []string{config.UpdatedAt},
   152  		)
   153  	}
   154  	return m.getSoftFieldNameAndType(
   155  		ctx, schema, tableName, updatedFieldNames,
   156  	)
   157  }
   158  
   159  // GetFieldNameAndTypeForDelete checks and returns the field name for record deleting time.
   160  // If there's no field name for storing deleting time, it returns an empty string.
   161  // It checks the key with or without cases or chars '-'/'_'/'.'/' '.
   162  func (m *softTimeMaintainer) GetFieldNameAndTypeForDelete(
   163  	ctx context.Context, schema string, table string,
   164  ) (fieldName string, fieldType LocalType) {
   165  	// It checks whether this feature disabled.
   166  	if m.db.GetConfig().TimeMaintainDisabled {
   167  		return "", LocalTypeUndefined
   168  	}
   169  	tableName := ""
   170  	if table != "" {
   171  		tableName = table
   172  	} else {
   173  		tableName = m.tablesInit
   174  	}
   175  	config := m.db.GetConfig()
   176  	if config.DeletedAt != "" {
   177  		return m.getSoftFieldNameAndType(
   178  			ctx, schema, tableName, []string{config.DeletedAt},
   179  		)
   180  	}
   181  	return m.getSoftFieldNameAndType(
   182  		ctx, schema, tableName, deletedFieldNames,
   183  	)
   184  }
   185  
   186  // getSoftFieldName retrieves and returns the field name of the table for possible key.
   187  func (m *softTimeMaintainer) getSoftFieldNameAndType(
   188  	ctx context.Context,
   189  	schema string, table string, checkFiledNames []string,
   190  ) (fieldName string, fieldType LocalType) {
   191  	var (
   192  		innerMemCache = m.db.GetCore().GetInnerMemCache()
   193  		cacheKey      = fmt.Sprintf(
   194  			`getSoftFieldNameAndType:%s#%s#%s`,
   195  			schema, table, strings.Join(checkFiledNames, "_"),
   196  		)
   197  		cacheDuration = gcache.DurationNoExpire
   198  		cacheFunc     = func(ctx context.Context) (value interface{}, err error) {
   199  			// Ignore the error from TableFields.
   200  			fieldsMap, err := m.TableFields(table, schema)
   201  			if err != nil {
   202  				return nil, err
   203  			}
   204  			if len(fieldsMap) == 0 {
   205  				return nil, nil
   206  			}
   207  			for _, checkFiledName := range checkFiledNames {
   208  				fieldName = searchFieldNameFromMap(fieldsMap, checkFiledName)
   209  				if fieldName != "" {
   210  					fieldType, _ = m.db.CheckLocalTypeForField(
   211  						ctx, fieldsMap[fieldName].Type, nil,
   212  					)
   213  					var cacheItem = getSoftFieldNameAndTypeCacheItem{
   214  						FieldName: fieldName,
   215  						FieldType: fieldType,
   216  					}
   217  					return cacheItem, nil
   218  				}
   219  			}
   220  			return
   221  		}
   222  	)
   223  	result, err := innerMemCache.GetOrSetFunc(
   224  		ctx, cacheKey, cacheFunc, cacheDuration,
   225  	)
   226  	if err != nil {
   227  		return
   228  	}
   229  	if result == nil {
   230  		return
   231  	}
   232  	cacheItem := result.Val().(getSoftFieldNameAndTypeCacheItem)
   233  	fieldName = cacheItem.FieldName
   234  	fieldType = cacheItem.FieldType
   235  	return
   236  }
   237  
   238  func searchFieldNameFromMap(fieldsMap map[string]*TableField, key string) string {
   239  	if len(fieldsMap) == 0 {
   240  		return ""
   241  	}
   242  	_, ok := fieldsMap[key]
   243  	if ok {
   244  		return key
   245  	}
   246  	key = utils.RemoveSymbols(key)
   247  	for k := range fieldsMap {
   248  		if strings.EqualFold(utils.RemoveSymbols(k), key) {
   249  			return k
   250  		}
   251  	}
   252  	return ""
   253  }
   254  
   255  // GetWhereConditionForDelete retrieves and returns the condition string for soft deleting.
   256  // It supports multiple tables string like:
   257  // "user u, user_detail ud"
   258  // "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)"
   259  // "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)"
   260  // "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)".
   261  func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) string {
   262  	if m.unscoped {
   263  		return ""
   264  	}
   265  	conditionArray := garray.NewStrArray()
   266  	if gstr.Contains(m.tables, " JOIN ") {
   267  		// Base table.
   268  		tableMatch, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables)
   269  		conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, tableMatch[1]))
   270  		// Multiple joined tables, exclude the sub query sql which contains char '(' and ')'.
   271  		tableMatches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables)
   272  		for _, match := range tableMatches {
   273  			conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, match[1]))
   274  		}
   275  	}
   276  	if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") {
   277  		// Multiple base tables.
   278  		for _, s := range gstr.SplitAndTrim(m.tables, ",") {
   279  			conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, s))
   280  		}
   281  	}
   282  	conditionArray.FilterEmpty()
   283  	if conditionArray.Len() > 0 {
   284  		return conditionArray.Join(" AND ")
   285  	}
   286  	// Only one table.
   287  	fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit)
   288  	if fieldName != "" {
   289  		return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType)
   290  	}
   291  	return ""
   292  }
   293  
   294  // getConditionOfTableStringForSoftDeleting does something as its name describes.
   295  // Examples for `s`:
   296  // - `test`.`demo` as b
   297  // - `test`.`demo` b
   298  // - `demo`
   299  // - demo
   300  func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string {
   301  	var (
   302  		table  string
   303  		schema string
   304  		array1 = gstr.SplitAndTrim(s, " ")
   305  		array2 = gstr.SplitAndTrim(array1[0], ".")
   306  	)
   307  	if len(array2) >= 2 {
   308  		table = array2[1]
   309  		schema = array2[0]
   310  	} else {
   311  		table = array2[0]
   312  	}
   313  	fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table)
   314  	if fieldName == "" {
   315  		return ""
   316  	}
   317  	if len(array1) >= 3 {
   318  		return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType)
   319  	}
   320  	if len(array1) >= 2 {
   321  		return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType)
   322  	}
   323  	return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType)
   324  }
   325  
   326  // GetDataByFieldNameAndTypeForDelete creates and returns the placeholder and value for
   327  // specified field name and type in soft-deleting scenario.
   328  func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete(
   329  	ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
   330  ) (dataHolder string, dataValue any) {
   331  	var (
   332  		quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
   333  		quotedFieldName   = m.db.GetCore().QuoteWord(fieldName)
   334  	)
   335  	if quotedFieldPrefix != "" {
   336  		quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
   337  	}
   338  	dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName)
   339  	dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false)
   340  	return
   341  }
   342  
   343  func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting(
   344  	ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
   345  ) string {
   346  	var (
   347  		quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
   348  		quotedFieldName   = m.db.GetCore().QuoteWord(fieldName)
   349  	)
   350  	if quotedFieldPrefix != "" {
   351  		quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
   352  	}
   353  	switch m.softTimeOption.SoftTimeType {
   354  	case SoftTimeTypeAuto:
   355  		switch fieldType {
   356  		case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
   357  			return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
   358  		case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64, LocalTypeBool:
   359  			return fmt.Sprintf(`%s=0`, quotedFieldName)
   360  		default:
   361  			intlog.Errorf(
   362  				ctx,
   363  				`invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`,
   364  				fieldType, fieldName, fieldPrefix,
   365  			)
   366  		}
   367  
   368  	case SoftTimeTypeTime:
   369  		return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
   370  
   371  	default:
   372  		return fmt.Sprintf(`%s=0`, quotedFieldName)
   373  	}
   374  	return ""
   375  }
   376  
   377  // GetValueByFieldTypeForCreateOrUpdate creates and returns the value for specified field type,
   378  // usually for creating or updating operations.
   379  func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate(
   380  	ctx context.Context, fieldType LocalType, isDeletedField bool,
   381  ) any {
   382  	var value any
   383  	if isDeletedField {
   384  		switch fieldType {
   385  		case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
   386  			value = nil
   387  		default:
   388  			value = 0
   389  		}
   390  		return value
   391  	}
   392  	switch m.softTimeOption.SoftTimeType {
   393  	case SoftTimeTypeAuto:
   394  		switch fieldType {
   395  		case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
   396  			value = gtime.Now()
   397  		case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64:
   398  			value = gtime.Timestamp()
   399  		case LocalTypeBool:
   400  			value = 1
   401  		default:
   402  			intlog.Errorf(
   403  				ctx,
   404  				`invalid field type "%s" for soft deleting data`,
   405  				fieldType,
   406  			)
   407  		}
   408  
   409  	default:
   410  		switch fieldType {
   411  		case LocalTypeBool:
   412  			value = 1
   413  		default:
   414  			value = m.createValueBySoftTimeOption(isDeletedField)
   415  		}
   416  	}
   417  	return value
   418  }
   419  
   420  func (m *softTimeMaintainer) createValueBySoftTimeOption(isDeletedField bool) any {
   421  	var value any
   422  	if isDeletedField {
   423  		switch m.softTimeOption.SoftTimeType {
   424  		case SoftTimeTypeTime:
   425  			value = nil
   426  		default:
   427  			value = 0
   428  		}
   429  		return value
   430  	}
   431  	switch m.softTimeOption.SoftTimeType {
   432  	case SoftTimeTypeTime:
   433  		value = gtime.Now()
   434  	case SoftTimeTypeTimestamp:
   435  		value = gtime.Timestamp()
   436  	case SoftTimeTypeTimestampMilli:
   437  		value = gtime.TimestampMilli()
   438  	case SoftTimeTypeTimestampMicro:
   439  		value = gtime.TimestampMicro()
   440  	case SoftTimeTypeTimestampNano:
   441  		value = gtime.TimestampNano()
   442  	default:
   443  		panic(gerror.NewCodef(
   444  			gcode.CodeInternalPanic,
   445  			`unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType,
   446  		))
   447  	}
   448  	return value
   449  }