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 }