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 }