github.com/wangyougui/gf/v2@v2.6.5/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/wangyougui/gf.
     6  
     7  package gdb
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  
    13  	"github.com/wangyougui/gf/v2/container/garray"
    14  	"github.com/wangyougui/gf/v2/errors/gcode"
    15  	"github.com/wangyougui/gf/v2/errors/gerror"
    16  	"github.com/wangyougui/gf/v2/internal/intlog"
    17  	"github.com/wangyougui/gf/v2/os/gcache"
    18  	"github.com/wangyougui/gf/v2/os/gtime"
    19  	"github.com/wangyougui/gf/v2/text/gregex"
    20  	"github.com/wangyougui/gf/v2/text/gstr"
    21  	"github.com/wangyougui/gf/v2/util/gconv"
    22  	"github.com/wangyougui/gf/v2/util/gutil"
    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  		cacheKey      = fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%p`, schema, table, checkFiledNames)
   193  		cacheDuration = gcache.DurationNoExpire
   194  		cacheFunc     = func(ctx context.Context) (value interface{}, err error) {
   195  			// Ignore the error from TableFields.
   196  			fieldsMap, _ := m.TableFields(table, schema)
   197  			if len(fieldsMap) > 0 {
   198  				for _, checkFiledName := range checkFiledNames {
   199  					fieldName, _ = gutil.MapPossibleItemByKey(
   200  						gconv.Map(fieldsMap), checkFiledName,
   201  					)
   202  					if fieldName != "" {
   203  						fieldType, _ = m.db.CheckLocalTypeForField(
   204  							ctx, fieldsMap[fieldName].Type, nil,
   205  						)
   206  						var cacheItem = getSoftFieldNameAndTypeCacheItem{
   207  							FieldName: fieldName,
   208  							FieldType: fieldType,
   209  						}
   210  						return cacheItem, nil
   211  					}
   212  				}
   213  			}
   214  			return
   215  		}
   216  	)
   217  	result, err := m.db.GetCache().GetOrSetFunc(ctx, cacheKey, cacheFunc, cacheDuration)
   218  	if err != nil {
   219  		intlog.Error(ctx, err)
   220  	}
   221  	if result != nil {
   222  		var cacheItem getSoftFieldNameAndTypeCacheItem
   223  		if err = result.Scan(&cacheItem); err != nil {
   224  			return "", ""
   225  		}
   226  		fieldName = cacheItem.FieldName
   227  		fieldType = cacheItem.FieldType
   228  	}
   229  	return
   230  }
   231  
   232  // GetWhereConditionForDelete retrieves and returns the condition string for soft deleting.
   233  // It supports multiple tables string like:
   234  // "user u, user_detail ud"
   235  // "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)"
   236  // "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)"
   237  // "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)".
   238  func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) string {
   239  	if m.unscoped {
   240  		return ""
   241  	}
   242  	conditionArray := garray.NewStrArray()
   243  	if gstr.Contains(m.tables, " JOIN ") {
   244  		// Base table.
   245  		tableMatch, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables)
   246  		conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, tableMatch[1]))
   247  		// Multiple joined tables, exclude the sub query sql which contains char '(' and ')'.
   248  		tableMatches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables)
   249  		for _, match := range tableMatches {
   250  			conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, match[1]))
   251  		}
   252  	}
   253  	if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") {
   254  		// Multiple base tables.
   255  		for _, s := range gstr.SplitAndTrim(m.tables, ",") {
   256  			conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, s))
   257  		}
   258  	}
   259  	conditionArray.FilterEmpty()
   260  	if conditionArray.Len() > 0 {
   261  		return conditionArray.Join(" AND ")
   262  	}
   263  	// Only one table.
   264  	fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit)
   265  	if fieldName != "" {
   266  		return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType)
   267  	}
   268  	return ""
   269  }
   270  
   271  // getConditionOfTableStringForSoftDeleting does something as its name describes.
   272  // Examples for `s`:
   273  // - `test`.`demo` as b
   274  // - `test`.`demo` b
   275  // - `demo`
   276  // - demo
   277  func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string {
   278  	var (
   279  		table  string
   280  		schema string
   281  		array1 = gstr.SplitAndTrim(s, " ")
   282  		array2 = gstr.SplitAndTrim(array1[0], ".")
   283  	)
   284  	if len(array2) >= 2 {
   285  		table = array2[1]
   286  		schema = array2[0]
   287  	} else {
   288  		table = array2[0]
   289  	}
   290  	fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table)
   291  	if fieldName == "" {
   292  		return ""
   293  	}
   294  	if len(array1) >= 3 {
   295  		return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType)
   296  	}
   297  	if len(array1) >= 2 {
   298  		return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType)
   299  	}
   300  	return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType)
   301  }
   302  
   303  // GetDataByFieldNameAndTypeForDelete creates and returns the placeholder and value for
   304  // specified field name and type in soft-deleting scenario.
   305  func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete(
   306  	ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
   307  ) (dataHolder string, dataValue any) {
   308  	var (
   309  		quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
   310  		quotedFieldName   = m.db.GetCore().QuoteWord(fieldName)
   311  	)
   312  	if quotedFieldPrefix != "" {
   313  		quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
   314  	}
   315  	dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName)
   316  	dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false)
   317  	return
   318  }
   319  
   320  func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting(
   321  	ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
   322  ) string {
   323  	var (
   324  		quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
   325  		quotedFieldName   = m.db.GetCore().QuoteWord(fieldName)
   326  	)
   327  	if quotedFieldPrefix != "" {
   328  		quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
   329  	}
   330  	switch m.softTimeOption.SoftTimeType {
   331  	case SoftTimeTypeAuto:
   332  		switch fieldType {
   333  		case LocalTypeDate, LocalTypeDatetime:
   334  			return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
   335  		case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeBool:
   336  			return fmt.Sprintf(`%s=0`, quotedFieldName)
   337  		default:
   338  			intlog.Errorf(
   339  				ctx,
   340  				`invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`,
   341  				fieldType, fieldName, fieldPrefix,
   342  			)
   343  		}
   344  
   345  	case SoftTimeTypeTime:
   346  		return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
   347  
   348  	default:
   349  		return fmt.Sprintf(`%s=0`, quotedFieldName)
   350  	}
   351  	return ""
   352  }
   353  
   354  // GetValueByFieldTypeForCreateOrUpdate creates and returns the value for specified field type,
   355  // usually for creating or updating operations.
   356  func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate(
   357  	ctx context.Context, fieldType LocalType, isDeletedField bool,
   358  ) any {
   359  	var value any
   360  	if isDeletedField {
   361  		switch fieldType {
   362  		case LocalTypeDate, LocalTypeDatetime:
   363  			value = nil
   364  		default:
   365  			value = 0
   366  		}
   367  		return value
   368  	}
   369  	switch m.softTimeOption.SoftTimeType {
   370  	case SoftTimeTypeAuto:
   371  		switch fieldType {
   372  		case LocalTypeDate, LocalTypeDatetime:
   373  			value = gtime.Now()
   374  		case LocalTypeInt, LocalTypeUint, LocalTypeInt64:
   375  			value = gtime.Timestamp()
   376  		case LocalTypeBool:
   377  			value = 1
   378  		default:
   379  			intlog.Errorf(
   380  				ctx,
   381  				`invalid field type "%s" for soft deleting data`,
   382  				fieldType,
   383  			)
   384  		}
   385  
   386  	default:
   387  		switch fieldType {
   388  		case LocalTypeBool:
   389  			value = 1
   390  		default:
   391  			value = m.createValueBySoftTimeOption(isDeletedField)
   392  		}
   393  	}
   394  	return value
   395  }
   396  
   397  func (m *softTimeMaintainer) createValueBySoftTimeOption(isDeletedField bool) any {
   398  	var value any
   399  	if isDeletedField {
   400  		switch m.softTimeOption.SoftTimeType {
   401  		case SoftTimeTypeTime:
   402  			value = nil
   403  		default:
   404  			value = 0
   405  		}
   406  		return value
   407  	}
   408  	switch m.softTimeOption.SoftTimeType {
   409  	case SoftTimeTypeTime:
   410  		value = gtime.Now()
   411  	case SoftTimeTypeTimestamp:
   412  		value = gtime.Timestamp()
   413  	case SoftTimeTypeTimestampMilli:
   414  		value = gtime.TimestampMilli()
   415  	case SoftTimeTypeTimestampMicro:
   416  		value = gtime.TimestampMicro()
   417  	case SoftTimeTypeTimestampNano:
   418  		value = gtime.TimestampNano()
   419  	default:
   420  		panic(gerror.NewCodef(
   421  			gcode.CodeInternalPanic,
   422  			`unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType,
   423  		))
   424  	}
   425  	return value
   426  }