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!
470 lines
13 KiB
Go
470 lines
13 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"
|
|
"database/sql"
|
|
"reflect"
|
|
|
|
"github.com/gogf/gf/v2/container/gset"
|
|
"github.com/gogf/gf/v2/errors/gcode"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/internal/empty"
|
|
"github.com/gogf/gf/v2/internal/reflection"
|
|
"github.com/gogf/gf/v2/text/gstr"
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
"github.com/gogf/gf/v2/util/gutil"
|
|
)
|
|
|
|
// Batch sets the batch operation number for the model.
|
|
func (m *Model) Batch(batch int) *Model {
|
|
model := m.getModel()
|
|
model.batch = batch
|
|
return model
|
|
}
|
|
|
|
// Data sets the operation data for the model.
|
|
// The parameter `data` can be type of string/map/gmap/slice/struct/*struct, etc.
|
|
// Note that, it uses shallow value copying for `data` if `data` is type of map/slice
|
|
// to avoid changing it inside function.
|
|
// Eg:
|
|
// Data("uid=10000")
|
|
// Data("uid", 10000)
|
|
// Data("uid=? AND name=?", 10000, "john")
|
|
// Data(g.Map{"uid": 10000, "name":"john"})
|
|
// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}).
|
|
func (m *Model) Data(data ...any) *Model {
|
|
var model = m.getModel()
|
|
if len(data) > 1 {
|
|
if s := gconv.String(data[0]); gstr.Contains(s, "?") {
|
|
model.data = s
|
|
model.extraArgs = data[1:]
|
|
} else {
|
|
newData := make(map[string]any)
|
|
for i := 0; i < len(data); i += 2 {
|
|
newData[gconv.String(data[i])] = data[i+1]
|
|
}
|
|
model.data = newData
|
|
}
|
|
} else if len(data) == 1 {
|
|
switch value := data[0].(type) {
|
|
case Result:
|
|
model.data = value.List()
|
|
|
|
case Record:
|
|
model.data = value.Map()
|
|
|
|
case List:
|
|
list := make(List, len(value))
|
|
for k, v := range value {
|
|
list[k] = gutil.MapCopy(v)
|
|
}
|
|
model.data = list
|
|
|
|
case Map:
|
|
model.data = gutil.MapCopy(value)
|
|
|
|
default:
|
|
reflectInfo := reflection.OriginValueAndKind(value)
|
|
switch reflectInfo.OriginKind {
|
|
case reflect.Slice, reflect.Array:
|
|
if reflectInfo.OriginValue.Len() > 0 {
|
|
// If the `data` parameter is a DO struct,
|
|
// it then adds `OmitNilData` option for this condition,
|
|
// which will filter all nil parameters in `data`.
|
|
if isDoStruct(reflectInfo.OriginValue.Index(0).Interface()) {
|
|
model = model.OmitNilData()
|
|
model.option |= optionOmitNilDataInternal
|
|
}
|
|
}
|
|
list := make(List, reflectInfo.OriginValue.Len())
|
|
for i := 0; i < reflectInfo.OriginValue.Len(); i++ {
|
|
list[i] = anyValueToMapBeforeToRecord(reflectInfo.OriginValue.Index(i).Interface())
|
|
}
|
|
model.data = list
|
|
|
|
case reflect.Struct:
|
|
// If the `data` parameter is a DO struct,
|
|
// it then adds `OmitNilData` option for this condition,
|
|
// which will filter all nil parameters in `data`.
|
|
if isDoStruct(value) {
|
|
model = model.OmitNilData()
|
|
}
|
|
if v, ok := data[0].(iInterfaces); ok {
|
|
var (
|
|
array = v.Interfaces()
|
|
list = make(List, len(array))
|
|
)
|
|
for i := 0; i < len(array); i++ {
|
|
list[i] = anyValueToMapBeforeToRecord(array[i])
|
|
}
|
|
model.data = list
|
|
} else {
|
|
model.data = anyValueToMapBeforeToRecord(data[0])
|
|
}
|
|
|
|
case reflect.Map:
|
|
model.data = anyValueToMapBeforeToRecord(data[0])
|
|
|
|
default:
|
|
model.data = data[0]
|
|
}
|
|
}
|
|
}
|
|
return model
|
|
}
|
|
|
|
// OnConflict sets the primary key or index when columns conflicts occurs.
|
|
// It's not necessary for MySQL driver.
|
|
func (m *Model) OnConflict(onConflict ...any) *Model {
|
|
if len(onConflict) == 0 {
|
|
return m
|
|
}
|
|
model := m.getModel()
|
|
if len(onConflict) > 1 {
|
|
model.onConflict = onConflict
|
|
} else if len(onConflict) == 1 {
|
|
model.onConflict = onConflict[0]
|
|
}
|
|
return model
|
|
}
|
|
|
|
// OnDuplicate sets the operations when columns conflicts occurs.
|
|
// In MySQL, this is used for "ON DUPLICATE KEY UPDATE" statement.
|
|
// In PgSQL, this is used for "ON CONFLICT (id) DO UPDATE SET" statement.
|
|
// The parameter `onDuplicate` can be type of string/Raw/*Raw/map/slice.
|
|
// Example:
|
|
//
|
|
// OnDuplicate("nickname, age")
|
|
// OnDuplicate("nickname", "age")
|
|
//
|
|
// OnDuplicate(g.Map{
|
|
// "nickname": gdb.Raw("CONCAT('name_', VALUES(`nickname`))"),
|
|
// })
|
|
//
|
|
// OnDuplicate(g.Map{
|
|
// "nickname": "passport",
|
|
// }).
|
|
func (m *Model) OnDuplicate(onDuplicate ...any) *Model {
|
|
if len(onDuplicate) == 0 {
|
|
return m
|
|
}
|
|
model := m.getModel()
|
|
if len(onDuplicate) > 1 {
|
|
model.onDuplicate = onDuplicate
|
|
} else if len(onDuplicate) == 1 {
|
|
model.onDuplicate = onDuplicate[0]
|
|
}
|
|
return model
|
|
}
|
|
|
|
// OnDuplicateEx sets the excluding columns for operations when columns conflict occurs.
|
|
// In MySQL, this is used for "ON DUPLICATE KEY UPDATE" statement.
|
|
// In PgSQL, this is used for "ON CONFLICT (id) DO UPDATE SET" statement.
|
|
// The parameter `onDuplicateEx` can be type of string/map/slice.
|
|
// Example:
|
|
//
|
|
// OnDuplicateEx("passport, password")
|
|
// OnDuplicateEx("passport", "password")
|
|
//
|
|
// OnDuplicateEx(g.Map{
|
|
// "passport": "",
|
|
// "password": "",
|
|
// }).
|
|
func (m *Model) OnDuplicateEx(onDuplicateEx ...any) *Model {
|
|
if len(onDuplicateEx) == 0 {
|
|
return m
|
|
}
|
|
model := m.getModel()
|
|
if len(onDuplicateEx) > 1 {
|
|
model.onDuplicateEx = onDuplicateEx
|
|
} else if len(onDuplicateEx) == 1 {
|
|
model.onDuplicateEx = onDuplicateEx[0]
|
|
}
|
|
return model
|
|
}
|
|
|
|
// Insert does "INSERT INTO ..." statement for the model.
|
|
// The optional parameter `data` is the same as the parameter of Model.Data function,
|
|
// see Model.Data.
|
|
func (m *Model) Insert(data ...any) (result sql.Result, err error) {
|
|
var ctx = m.GetCtx()
|
|
if len(data) > 0 {
|
|
return m.Data(data...).Insert()
|
|
}
|
|
return m.doInsertWithOption(ctx, InsertOptionDefault)
|
|
}
|
|
|
|
// InsertAndGetId performs action Insert and returns the last insert id that automatically generated.
|
|
func (m *Model) InsertAndGetId(data ...any) (lastInsertId int64, err error) {
|
|
var ctx = m.GetCtx()
|
|
if len(data) > 0 {
|
|
return m.Data(data...).InsertAndGetId()
|
|
}
|
|
result, err := m.doInsertWithOption(ctx, InsertOptionDefault)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.LastInsertId()
|
|
}
|
|
|
|
// InsertIgnore does "INSERT IGNORE INTO ..." statement for the model.
|
|
// The optional parameter `data` is the same as the parameter of Model.Data function,
|
|
// see Model.Data.
|
|
func (m *Model) InsertIgnore(data ...any) (result sql.Result, err error) {
|
|
var ctx = m.GetCtx()
|
|
if len(data) > 0 {
|
|
return m.Data(data...).InsertIgnore()
|
|
}
|
|
return m.doInsertWithOption(ctx, InsertOptionIgnore)
|
|
}
|
|
|
|
// Replace does "REPLACE INTO ..." statement for the model.
|
|
// The optional parameter `data` is the same as the parameter of Model.Data function,
|
|
// see Model.Data.
|
|
func (m *Model) Replace(data ...any) (result sql.Result, err error) {
|
|
var ctx = m.GetCtx()
|
|
if len(data) > 0 {
|
|
return m.Data(data...).Replace()
|
|
}
|
|
return m.doInsertWithOption(ctx, InsertOptionReplace)
|
|
}
|
|
|
|
// Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the model.
|
|
// The optional parameter `data` is the same as the parameter of Model.Data function,
|
|
// see Model.Data.
|
|
//
|
|
// It updates the record if there's primary or unique index in the saving data,
|
|
// or else it inserts a new record into the table.
|
|
func (m *Model) Save(data ...any) (result sql.Result, err error) {
|
|
var ctx = m.GetCtx()
|
|
if len(data) > 0 {
|
|
return m.Data(data...).Save()
|
|
}
|
|
return m.doInsertWithOption(ctx, InsertOptionSave)
|
|
}
|
|
|
|
// doInsertWithOption inserts data with option parameter.
|
|
func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOption) (result sql.Result, err error) {
|
|
defer func() {
|
|
if err == nil {
|
|
m.checkAndRemoveSelectCache(ctx)
|
|
}
|
|
}()
|
|
if m.data == nil {
|
|
return nil, gerror.NewCode(gcode.CodeMissingParameter, "inserting into table with empty data")
|
|
}
|
|
var (
|
|
list List
|
|
stm = m.softTimeMaintainer()
|
|
fieldNameCreate, fieldTypeCreate = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldCreate)
|
|
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldUpdate)
|
|
fieldNameDelete, fieldTypeDelete = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldDelete)
|
|
)
|
|
// m.data was already converted to type List/Map by function Data
|
|
newData, err := m.filterDataForInsertOrUpdate(m.data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// It converts any data to List type for inserting.
|
|
switch value := newData.(type) {
|
|
case List:
|
|
list = value
|
|
|
|
case Map:
|
|
list = List{value}
|
|
}
|
|
|
|
if len(list) < 1 {
|
|
return result, gerror.NewCode(gcode.CodeMissingParameter, "data list cannot be empty")
|
|
}
|
|
|
|
// Automatic handling for creating/updating time.
|
|
if fieldNameCreate != "" && m.isFieldInFieldsEx(fieldNameCreate) {
|
|
fieldNameCreate = ""
|
|
}
|
|
if fieldNameUpdate != "" && m.isFieldInFieldsEx(fieldNameUpdate) {
|
|
fieldNameUpdate = ""
|
|
}
|
|
var isSoftTimeFeatureEnabled = fieldNameCreate != "" || fieldNameUpdate != ""
|
|
if !m.unscoped && isSoftTimeFeatureEnabled {
|
|
for k, v := range list {
|
|
if fieldNameCreate != "" && empty.IsNil(v[fieldNameCreate]) {
|
|
fieldCreateValue := stm.GetFieldValue(ctx, fieldTypeCreate, false)
|
|
if fieldCreateValue != nil {
|
|
v[fieldNameCreate] = fieldCreateValue
|
|
}
|
|
}
|
|
if fieldNameUpdate != "" && empty.IsNil(v[fieldNameUpdate]) {
|
|
fieldUpdateValue := stm.GetFieldValue(ctx, fieldTypeUpdate, false)
|
|
if fieldUpdateValue != nil {
|
|
v[fieldNameUpdate] = fieldUpdateValue
|
|
}
|
|
}
|
|
// for timestamp field that should initialize the delete_at field with value, for example 0.
|
|
if fieldNameDelete != "" && empty.IsNil(v[fieldNameDelete]) {
|
|
fieldDeleteValue := stm.GetFieldValue(ctx, fieldTypeDelete, true)
|
|
if fieldDeleteValue != nil {
|
|
v[fieldNameDelete] = fieldDeleteValue
|
|
}
|
|
}
|
|
list[k] = v
|
|
}
|
|
}
|
|
// Format DoInsertOption, especially for "ON DUPLICATE KEY UPDATE" statement.
|
|
columnNames := make([]string, 0, len(list[0]))
|
|
for k := range list[0] {
|
|
columnNames = append(columnNames, k)
|
|
}
|
|
doInsertOption, err := m.formatDoInsertOption(insertOption, columnNames)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
in := &HookInsertInput{
|
|
internalParamHookInsert: internalParamHookInsert{
|
|
internalParamHook: internalParamHook{
|
|
link: m.getLink(true),
|
|
},
|
|
handler: m.hookHandler.Insert,
|
|
},
|
|
Model: m,
|
|
Table: m.tables,
|
|
Schema: m.schema,
|
|
Data: list,
|
|
Option: doInsertOption,
|
|
}
|
|
return in.Next(ctx)
|
|
}
|
|
|
|
func (m *Model) formatDoInsertOption(insertOption InsertOption, columnNames []string) (option DoInsertOption, err error) {
|
|
option = DoInsertOption{
|
|
InsertOption: insertOption,
|
|
BatchCount: m.getBatch(),
|
|
}
|
|
if insertOption != InsertOptionSave {
|
|
return
|
|
}
|
|
|
|
onConflictKeys, err := m.formatOnConflictKeys(m.onConflict)
|
|
if err != nil {
|
|
return option, err
|
|
}
|
|
option.OnConflict = onConflictKeys
|
|
|
|
onDuplicateExKeys, err := m.formatOnDuplicateExKeys(m.onDuplicateEx)
|
|
if err != nil {
|
|
return option, err
|
|
}
|
|
onDuplicateExKeySet := gset.NewStrSetFrom(onDuplicateExKeys)
|
|
if m.onDuplicate != nil {
|
|
switch m.onDuplicate.(type) {
|
|
case Raw, *Raw:
|
|
option.OnDuplicateStr = gconv.String(m.onDuplicate)
|
|
|
|
default:
|
|
reflectInfo := reflection.OriginValueAndKind(m.onDuplicate)
|
|
switch reflectInfo.OriginKind {
|
|
case reflect.String:
|
|
option.OnDuplicateMap = make(map[string]any)
|
|
for _, v := range gstr.SplitAndTrim(reflectInfo.OriginValue.String(), ",") {
|
|
if onDuplicateExKeySet.Contains(v) {
|
|
continue
|
|
}
|
|
option.OnDuplicateMap[v] = v
|
|
}
|
|
|
|
case reflect.Map:
|
|
option.OnDuplicateMap = make(map[string]any)
|
|
for k, v := range gconv.Map(m.onDuplicate) {
|
|
if onDuplicateExKeySet.Contains(k) {
|
|
continue
|
|
}
|
|
option.OnDuplicateMap[k] = v
|
|
}
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
option.OnDuplicateMap = make(map[string]any)
|
|
for _, v := range gconv.Strings(m.onDuplicate) {
|
|
if onDuplicateExKeySet.Contains(v) {
|
|
continue
|
|
}
|
|
option.OnDuplicateMap[v] = v
|
|
}
|
|
|
|
default:
|
|
return option, gerror.NewCodef(
|
|
gcode.CodeInvalidParameter,
|
|
`unsupported OnDuplicate parameter type "%s"`,
|
|
reflect.TypeOf(m.onDuplicate),
|
|
)
|
|
}
|
|
}
|
|
} else if onDuplicateExKeySet.Size() > 0 {
|
|
option.OnDuplicateMap = make(map[string]any)
|
|
for _, v := range columnNames {
|
|
if onDuplicateExKeySet.Contains(v) {
|
|
continue
|
|
}
|
|
option.OnDuplicateMap[v] = v
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *Model) formatOnDuplicateExKeys(onDuplicateEx any) ([]string, error) {
|
|
if onDuplicateEx == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
reflectInfo := reflection.OriginValueAndKind(onDuplicateEx)
|
|
switch reflectInfo.OriginKind {
|
|
case reflect.String:
|
|
return gstr.SplitAndTrim(reflectInfo.OriginValue.String(), ","), nil
|
|
|
|
case reflect.Map:
|
|
return gutil.Keys(onDuplicateEx), nil
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
return gconv.Strings(onDuplicateEx), nil
|
|
|
|
default:
|
|
return nil, gerror.NewCodef(
|
|
gcode.CodeInvalidParameter,
|
|
`unsupported OnDuplicateEx parameter type "%s"`,
|
|
reflect.TypeOf(onDuplicateEx),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (m *Model) formatOnConflictKeys(onConflict any) ([]string, error) {
|
|
if onConflict == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
reflectInfo := reflection.OriginValueAndKind(onConflict)
|
|
switch reflectInfo.OriginKind {
|
|
case reflect.String:
|
|
return gstr.SplitAndTrim(reflectInfo.OriginValue.String(), ","), nil
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
return gconv.Strings(onConflict), nil
|
|
|
|
default:
|
|
return nil, gerror.NewCodef(
|
|
gcode.CodeInvalidParameter,
|
|
`unsupported onConflict parameter type "%s"`,
|
|
reflect.TypeOf(onConflict),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (m *Model) getBatch() int {
|
|
return m.batch
|
|
}
|