add field type detection for soft time field like created_at/updated_at/deleted_at to support unix timestamp or bool deleting table field (#3293)

This commit is contained in:
John Guo 2024-02-06 10:21:23 +08:00 committed by GitHub
parent 85c5b7f19e
commit 2acdf4bb47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 791 additions and 248 deletions

View File

@ -11,14 +11,15 @@ import (
"testing"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
// CreateAt/UpdateAt/DeleteAt.
func Test_SoftCreateUpdateDeleteTimeMicroSecond(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
func Test_SoftTime_CreateUpdateDelete1(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -151,8 +152,8 @@ CREATE TABLE %s (
}
// CreateAt/UpdateAt/DeleteAt.
func Test_SoftCreateUpdateDeleteTimeSecond(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
func Test_SoftTime_CreateUpdateDelete2(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -285,8 +286,8 @@ CREATE TABLE %s (
}
// CreatedAt/UpdatedAt/DeletedAt.
func Test_SoftCreatedUpdatedDeletedTime_Map(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
func Test_SoftTime_CreatedUpdatedDeleted_Map(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -419,8 +420,8 @@ CREATE TABLE %s (
}
// CreatedAt/UpdatedAt/DeletedAt.
func Test_SoftCreatedUpdatedDeletedTime_Struct(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
func Test_SoftTime_CreatedUpdatedDeleted_Struct(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -560,7 +561,7 @@ CREATE TABLE %s (
}
func Test_SoftUpdateTime(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -600,7 +601,7 @@ CREATE TABLE %s (
}
func Test_SoftUpdateTime_WithDO(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -657,7 +658,7 @@ CREATE TABLE %s (
}
func Test_SoftDelete(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -796,7 +797,7 @@ CREATE TABLE %s (
}
func Test_SoftDelete_WhereAndOr(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -838,7 +839,7 @@ CREATE TABLE %s (
}
func Test_CreateUpdateTime_Struct(t *testing.T) {
table := "time_test_table_" + gtime.TimestampNanoStr()
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
@ -989,3 +990,280 @@ CREATE TABLE %s (
t.Assert(i, 0)
})
}
func Test_SoftTime_CreateUpdateDelete_UnixTimestamp(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
name varchar(45) DEFAULT NULL,
create_at int(11) DEFAULT NULL,
update_at int(11) DEFAULT NULL,
delete_at int(11) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, table)); err != nil {
gtest.Error(err)
}
defer dropTable(table)
// insert
gtest.C(t, func(t *gtest.T) {
dataInsert := g.Map{
"id": 1,
"name": "name_1",
}
r, err := db.Model(table).Data(dataInsert).Insert()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.Assert(one["delete_at"].Int64(), 0)
t.Assert(len(one["create_at"].String()), 10)
t.Assert(len(one["update_at"].String()), 10)
})
// sleep some seconds to make update time greater than create time.
time.Sleep(2 * time.Second)
// update
gtest.C(t, func(t *gtest.T) {
// update: map
dataInsert := g.Map{
"name": "name_11",
}
r, err := db.Model(table).Data(dataInsert).WherePri(1).Update()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_11")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.Assert(one["delete_at"].Int64(), 0)
t.Assert(len(one["create_at"].String()), 10)
t.Assert(len(one["update_at"].String()), 10)
var (
lastCreateTime = one["create_at"].Int64()
lastUpdateTime = one["update_at"].Int64()
)
time.Sleep(2 * time.Second)
// update: string
r, err = db.Model(table).Data("name='name_111'").WherePri(1).Update()
t.AssertNil(err)
n, _ = r.RowsAffected()
t.Assert(n, 1)
one, err = db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_111")
t.Assert(one["create_at"].Int64(), lastCreateTime)
t.AssertGT(one["update_at"].Int64(), lastUpdateTime)
t.Assert(one["delete_at"].Int64(), 0)
})
// delete
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).WherePri(1).Delete()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(len(one), 0)
one, err = db.Model(table).Unscoped().WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_111")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.AssertGT(one["delete_at"].Int64(), 0)
})
}
func Test_SoftTime_CreateUpdateDelete_Bool_Deleted(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
name varchar(45) DEFAULT NULL,
create_at int(11) DEFAULT NULL,
update_at int(11) DEFAULT NULL,
delete_at bit(1) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, table)); err != nil {
gtest.Error(err)
}
defer dropTable(table)
//db.SetDebug(true)
// insert
gtest.C(t, func(t *gtest.T) {
dataInsert := g.Map{
"id": 1,
"name": "name_1",
}
r, err := db.Model(table).Data(dataInsert).Insert()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.Assert(one["delete_at"].Int64(), 0)
t.Assert(len(one["create_at"].String()), 10)
t.Assert(len(one["update_at"].String()), 10)
})
// delete
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).WherePri(1).Delete()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(len(one), 0)
one, err = db.Model(table).Unscoped().WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.Assert(one["delete_at"].Int64(), 1)
})
}
func Test_SoftTime_CreateUpdateDelete_Option_SoftTimeTypeTimestampMilli(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
name varchar(45) DEFAULT NULL,
create_at bigint(19) unsigned DEFAULT NULL,
update_at bigint(19) unsigned DEFAULT NULL,
delete_at bit(1) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, table)); err != nil {
gtest.Error(err)
}
defer dropTable(table)
var softTimeOption = gdb.SoftTimeOption{
SoftTimeType: gdb.SoftTimeTypeTimestampMilli,
}
// insert
gtest.C(t, func(t *gtest.T) {
dataInsert := g.Map{
"id": 1,
"name": "name_1",
}
r, err := db.Model(table).SoftTime(softTimeOption).Data(dataInsert).Insert()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.Assert(len(one["create_at"].String()), 13)
t.Assert(len(one["update_at"].String()), 13)
t.Assert(one["delete_at"].Int64(), 0)
})
// delete
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).Delete()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One()
t.AssertNil(err)
t.Assert(len(one), 0)
one, err = db.Model(table).Unscoped().WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.Assert(one["delete_at"].Int64(), 1)
})
}
func Test_SoftTime_CreateUpdateDelete_Option_SoftTimeTypeTimestampNano(t *testing.T) {
table := "soft_time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,
name varchar(45) DEFAULT NULL,
create_at bigint(19) unsigned DEFAULT NULL,
update_at bigint(19) unsigned DEFAULT NULL,
delete_at bit(1) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, table)); err != nil {
gtest.Error(err)
}
defer dropTable(table)
var softTimeOption = gdb.SoftTimeOption{
SoftTimeType: gdb.SoftTimeTypeTimestampNano,
}
// insert
gtest.C(t, func(t *gtest.T) {
dataInsert := g.Map{
"id": 1,
"name": "name_1",
}
r, err := db.Model(table).SoftTime(softTimeOption).Data(dataInsert).Insert()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.Assert(len(one["create_at"].String()), 19)
t.Assert(len(one["update_at"].String()), 19)
t.Assert(one["delete_at"].Int64(), 0)
})
// delete
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).Delete()
t.AssertNil(err)
n, _ := r.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).SoftTime(softTimeOption).WherePri(1).One()
t.AssertNil(err)
t.Assert(len(one), 0)
one, err = db.Model(table).Unscoped().WherePri(1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), "name_1")
t.AssertGT(one["create_at"].Int64(), 0)
t.AssertGT(one["update_at"].Int64(), 0)
t.Assert(one["delete_at"].Int64(), 1)
})
}

View File

@ -425,6 +425,7 @@ const (
type LocalType string
const (
LocalTypeUndefined LocalType = ""
LocalTypeString LocalType = "string"
LocalTypeDate LocalType = "date"
LocalTypeDatetime LocalType = "datetime"

View File

@ -299,7 +299,9 @@ func (c *Core) CheckLocalTypeForField(ctx context.Context, fieldType string, fie
// ConvertValueForLocal converts value to local Golang type of value according field type name from database.
// The parameter `fieldType` is in lower case, like:
// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc.
func (c *Core) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) {
func (c *Core) ConvertValueForLocal(
ctx context.Context, fieldType string, fieldValue interface{},
) (interface{}, error) {
// If there's no type retrieved, it returns the `fieldValue` directly
// to use its original data type, as `fieldValue` is type of interface{}.
if fieldType == "" {

View File

@ -17,40 +17,41 @@ import (
// Model is core struct implementing the DAO for ORM.
type Model struct {
db DB // Underlying DB interface.
tx TX // Underlying TX interface.
rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model.
schema string // Custom database schema.
linkType int // Mark for operation on master or slave.
tablesInit string // Table names when model initialization.
tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud".
fields string // Operation fields, multiple fields joined using char ','.
fieldsEx string // Excluded operation fields, multiple fields joined using char ','.
withArray []interface{} // Arguments for With feature.
withAll bool // Enable model association operations on all objects that have "with" tag in the struct.
extraArgs []interface{} // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver.
whereBuilder *WhereBuilder // Condition builder for where operation.
groupBy string // Used for "group by" statement.
orderBy string // Used for "order by" statement.
having []interface{} // Used for "having..." statement.
start int // Used for "select ... start, limit ..." statement.
limit int // Used for "select ... start, limit ..." statement.
option int // Option for extra operation features.
offset int // Offset statement for some databases grammar.
partition string // Partition table partition name.
data interface{} // Data for operation, which can be type of map/[]map/struct/*struct/string, etc.
batch int // Batch number for batch Insert/Replace/Save operations.
filter bool // Filter data and where key-value pairs according to the fields of the table.
distinct string // Force the query to only return distinct results.
lockInfo string // Lock for update or in shared lock.
cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage.
cacheOption CacheOption // Cache option for query statement.
hookHandler HookHandler // Hook functions for model hook feature.
unscoped bool // Disables soft deleting features when select/delete operations.
safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model.
onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement.
onDuplicateEx interface{} // onDuplicateEx is used for excluding some columns ON "DUPLICATE KEY UPDATE" statement.
tableAliasMap map[string]string // Table alias to true table name, usually used in join statements.
db DB // Underlying DB interface.
tx TX // Underlying TX interface.
rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model.
schema string // Custom database schema.
linkType int // Mark for operation on master or slave.
tablesInit string // Table names when model initialization.
tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud".
fields string // Operation fields, multiple fields joined using char ','.
fieldsEx string // Excluded operation fields, multiple fields joined using char ','.
withArray []interface{} // Arguments for With feature.
withAll bool // Enable model association operations on all objects that have "with" tag in the struct.
extraArgs []interface{} // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver.
whereBuilder *WhereBuilder // Condition builder for where operation.
groupBy string // Used for "group by" statement.
orderBy string // Used for "order by" statement.
having []interface{} // Used for "having..." statement.
start int // Used for "select ... start, limit ..." statement.
limit int // Used for "select ... start, limit ..." statement.
option int // Option for extra operation features.
offset int // Offset statement for some databases grammar.
partition string // Partition table partition name.
data interface{} // Data for operation, which can be type of map/[]map/struct/*struct/string, etc.
batch int // Batch number for batch Insert/Replace/Save operations.
filter bool // Filter data and where key-value pairs according to the fields of the table.
distinct string // Force the query to only return distinct results.
lockInfo string // Lock for update or in shared lock.
cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage.
cacheOption CacheOption // Cache option for query statement.
hookHandler HookHandler // Hook functions for model hook feature.
unscoped bool // Disables soft deleting features when select/delete operations.
safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model.
onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement.
onDuplicateEx interface{} // onDuplicateEx is used for excluding some columns ON "DUPLICATE KEY UPDATE" statement.
tableAliasMap map[string]string // Table alias to true table name, usually used in join statements.
softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model.
}
// ModelHandler is a function that handles given Model and returns a new Model that is custom modified.

View File

@ -8,13 +8,11 @@ package gdb
import (
"database/sql"
"fmt"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/os/gtime"
)
// Delete does "DELETE FROM ... " statement for the model.
@ -31,9 +29,11 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
}
}()
var (
fieldNameDelete = m.getSoftFieldNameDeleted("", m.tablesInit)
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false)
conditionStr = conditionWhere + conditionExtra
fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetFieldNameAndTypeForDelete(
ctx, "", m.tablesInit,
)
)
if m.unscoped {
fieldNameDelete = ""
@ -52,6 +52,9 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
// Soft deleting.
if fieldNameDelete != "" {
dataHolder, dataValue := m.softTimeMaintainer().GetDataByFieldNameAndTypeForDelete(
ctx, "", fieldNameDelete, fieldTypeDelete,
)
in := &HookUpdateInput{
internalParamHookUpdate: internalParamHookUpdate{
internalParamHook: internalParamHook{
@ -61,9 +64,9 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
},
Model: m,
Table: m.tables,
Data: fmt.Sprintf(`%s=?`, m.db.GetCore().QuoteString(fieldNameDelete)),
Data: dataHolder,
Condition: conditionStr,
Args: append([]interface{}{gtime.Now()}, conditionArgs...),
Args: append([]interface{}{dataValue}, conditionArgs...),
}
return in.Next(ctx)
}

View File

@ -15,7 +15,6 @@ import (
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gutil"
@ -243,10 +242,11 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio
return nil, gerror.NewCode(gcode.CodeMissingParameter, "inserting into table with empty data")
}
var (
list List
now = gtime.Now()
fieldNameCreate = m.getSoftFieldNameCreated("", m.tablesInit)
fieldNameUpdate = m.getSoftFieldNameUpdated("", m.tablesInit)
list List
stm = m.softTimeMaintainer()
fieldNameCreate, fieldTypeCreate = stm.GetFieldNameAndTypeForCreate(ctx, "", m.tablesInit)
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate(ctx, "", m.tablesInit)
fieldNameDelete, fieldTypeDelete = stm.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit)
)
// m.data was already converted to type List/Map by function Data
newData, err := m.filterDataForInsertOrUpdate(m.data)
@ -270,10 +270,22 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio
if !m.unscoped && (fieldNameCreate != "" || fieldNameUpdate != "") {
for k, v := range list {
if fieldNameCreate != "" {
v[fieldNameCreate] = now
fieldCreateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate, false)
if fieldCreateValue != nil {
v[fieldNameCreate] = fieldCreateValue
}
}
if fieldNameUpdate != "" {
v[fieldNameUpdate] = now
fieldUpdateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false)
if fieldUpdateValue != nil {
v[fieldNameUpdate] = fieldUpdateValue
}
}
if fieldNameDelete != "" {
fieldDeleteValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeDelete, true)
if fieldDeleteValue != nil {
v[fieldNameDelete] = fieldDeleteValue
}
}
list[k] = v
}

View File

@ -730,7 +730,7 @@ func (m *Model) formatCondition(
}
// WHERE
conditionWhere, conditionArgs = m.whereBuilder.Build()
softDeletingCondition := m.getConditionForSoftDeleting()
softDeletingCondition := m.softTimeMaintainer().GetWhereConditionForDelete(ctx)
if m.rawSql != "" && conditionWhere != "" {
if gstr.ContainsI(m.rawSql, " WHERE ") {
conditionWhere = " AND " + conditionWhere

View File

@ -0,0 +1,423 @@
// 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"
"fmt"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gutil"
)
// SoftTimeType custom defines the soft time field type.
type SoftTimeType int
const (
SoftTimeTypeAuto SoftTimeType = 0 // (Default)Auto detect the field type by table field type.
SoftTimeTypeTime SoftTimeType = 1 // Using datetime as the field value.
SoftTimeTypeTimestamp SoftTimeType = 2 // In unix seconds.
SoftTimeTypeTimestampMilli SoftTimeType = 3 // In unix milliseconds.
SoftTimeTypeTimestampMicro SoftTimeType = 4 // In unix microseconds.
SoftTimeTypeTimestampNano SoftTimeType = 5 // In unix nanoseconds.
)
// SoftTimeOption is the option to customize soft time feature for Model.
type SoftTimeOption struct {
SoftTimeType SoftTimeType // The value type for soft time field.
}
type softTimeMaintainer struct {
*Model
}
type iSoftTimeMaintainer interface {
GetFieldNameAndTypeForCreate(
ctx context.Context, schema string, table string,
) (fieldName string, fieldType LocalType)
GetFieldNameAndTypeForUpdate(
ctx context.Context, schema string, table string,
) (fieldName string, fieldType LocalType)
GetFieldNameAndTypeForDelete(
ctx context.Context, schema string, table string,
) (fieldName string, fieldType LocalType)
GetValueByFieldTypeForCreateOrUpdate(
ctx context.Context, fieldType LocalType, isDeletedField bool,
) (dataValue any)
GetDataByFieldNameAndTypeForDelete(
ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
) (dataHolder string, dataValue any)
GetWhereConditionForDelete(ctx context.Context) string
}
// getSoftFieldNameAndTypeCacheItem is the internal struct for storing create/update/delete fields.
type getSoftFieldNameAndTypeCacheItem struct {
FieldName string
FieldType LocalType
}
var (
// Default field names of table for automatic-filled for record creating.
createdFieldNames = []string{"created_at", "create_at"}
// Default field names of table for automatic-filled for record updating.
updatedFieldNames = []string{"updated_at", "update_at"}
// Default field names of table for automatic-filled for record deleting.
deletedFieldNames = []string{"deleted_at", "delete_at"}
)
// SoftTime sets the SoftTimeOption to customize soft time feature for Model.
func (m *Model) SoftTime(option SoftTimeOption) *Model {
model := m.getModel()
model.softTimeOption = option
return model
}
// Unscoped disables the soft time feature for insert, update and delete operations.
func (m *Model) Unscoped() *Model {
model := m.getModel()
model.unscoped = true
return model
}
func (m *Model) softTimeMaintainer() iSoftTimeMaintainer {
return &softTimeMaintainer{
m,
}
}
// GetFieldNameAndTypeForCreate checks and returns the field name for record creating time.
// If there's no field name for storing creating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *softTimeMaintainer) GetFieldNameAndTypeForCreate(
ctx context.Context, schema string, table string,
) (fieldName string, fieldType LocalType) {
// It checks whether this feature disabled.
if m.db.GetConfig().TimeMaintainDisabled {
return "", LocalTypeUndefined
}
tableName := ""
if table != "" {
tableName = table
} else {
tableName = m.tablesInit
}
config := m.db.GetConfig()
if config.CreatedAt != "" {
return m.getSoftFieldNameAndType(
ctx, schema, tableName, []string{config.CreatedAt},
)
}
return m.getSoftFieldNameAndType(
ctx, schema, tableName, createdFieldNames,
)
}
// GetFieldNameAndTypeForUpdate checks and returns the field name for record updating time.
// If there's no field name for storing updating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *softTimeMaintainer) GetFieldNameAndTypeForUpdate(
ctx context.Context, schema string, table string,
) (fieldName string, fieldType LocalType) {
// It checks whether this feature disabled.
if m.db.GetConfig().TimeMaintainDisabled {
return "", LocalTypeUndefined
}
tableName := ""
if table != "" {
tableName = table
} else {
tableName = m.tablesInit
}
config := m.db.GetConfig()
if config.UpdatedAt != "" {
return m.getSoftFieldNameAndType(
ctx, schema, tableName, []string{config.UpdatedAt},
)
}
return m.getSoftFieldNameAndType(
ctx, schema, tableName, updatedFieldNames,
)
}
// GetFieldNameAndTypeForDelete checks and returns the field name for record deleting time.
// If there's no field name for storing deleting time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *softTimeMaintainer) GetFieldNameAndTypeForDelete(
ctx context.Context, schema string, table string,
) (fieldName string, fieldType LocalType) {
// It checks whether this feature disabled.
if m.db.GetConfig().TimeMaintainDisabled {
return "", LocalTypeUndefined
}
tableName := ""
if table != "" {
tableName = table
} else {
tableName = m.tablesInit
}
config := m.db.GetConfig()
if config.DeletedAt != "" {
return m.getSoftFieldNameAndType(
ctx, schema, tableName, []string{config.DeletedAt},
)
}
return m.getSoftFieldNameAndType(
ctx, schema, tableName, deletedFieldNames,
)
}
// getSoftFieldName retrieves and returns the field name of the table for possible key.
func (m *softTimeMaintainer) getSoftFieldNameAndType(
ctx context.Context,
schema string, table string, checkFiledNames []string,
) (fieldName string, fieldType LocalType) {
var (
cacheKey = fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%p`, schema, table, checkFiledNames)
cacheDuration = gcache.DurationNoExpire
cacheFunc = func(ctx context.Context) (value interface{}, err error) {
// Ignore the error from TableFields.
fieldsMap, _ := m.TableFields(table, schema)
if len(fieldsMap) > 0 {
for _, checkFiledName := range checkFiledNames {
fieldName, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), checkFiledName,
)
if fieldName != "" {
fieldType, _ = m.db.CheckLocalTypeForField(
ctx, fieldsMap[fieldName].Type, nil,
)
var cacheItem = getSoftFieldNameAndTypeCacheItem{
FieldName: fieldName,
FieldType: fieldType,
}
return cacheItem, nil
}
}
}
return
}
)
result, err := m.db.GetCache().GetOrSetFunc(ctx, cacheKey, cacheFunc, cacheDuration)
if err != nil {
intlog.Error(ctx, err)
}
if result != nil {
var cacheItem = result.Val().(getSoftFieldNameAndTypeCacheItem)
fieldName = cacheItem.FieldName
fieldType = cacheItem.FieldType
}
return
}
// GetWhereConditionForDelete retrieves and returns the condition string for soft deleting.
// It supports multiple tables string like:
// "user u, user_detail ud"
// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)"
// "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)"
// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)".
func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) string {
if m.unscoped {
return ""
}
conditionArray := garray.NewStrArray()
if gstr.Contains(m.tables, " JOIN ") {
// Base table.
tableMatch, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables)
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, tableMatch[1]))
// Multiple joined tables, exclude the sub query sql which contains char '(' and ')'.
tableMatches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables)
for _, match := range tableMatches {
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, match[1]))
}
}
if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") {
// Multiple base tables.
for _, s := range gstr.SplitAndTrim(m.tables, ",") {
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, s))
}
}
conditionArray.FilterEmpty()
if conditionArray.Len() > 0 {
return conditionArray.Join(" AND ")
}
// Only one table.
fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit)
if fieldName != "" {
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType)
}
return ""
}
// getConditionOfTableStringForSoftDeleting does something as its name describes.
// Examples for `s`:
// - `test`.`demo` as b
// - `test`.`demo` b
// - `demo`
// - demo
func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string {
var (
table string
schema string
array1 = gstr.SplitAndTrim(s, " ")
array2 = gstr.SplitAndTrim(array1[0], ".")
)
if len(array2) >= 2 {
table = array2[1]
schema = array2[0]
} else {
table = array2[0]
}
fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table)
if fieldName == "" {
return ""
}
if len(array1) >= 3 {
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType)
}
if len(array1) >= 2 {
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType)
}
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType)
}
// GetDataByFieldNameAndTypeForDelete creates and returns the placeholder and value for
// specified field name and type in soft-deleting scenario.
func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete(
ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
) (dataHolder string, dataValue any) {
var (
quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
quotedFieldName = m.db.GetCore().QuoteWord(fieldName)
)
if quotedFieldPrefix != "" {
quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
}
dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName)
dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false)
return
}
func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting(
ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
) string {
var (
quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
quotedFieldName = m.db.GetCore().QuoteWord(fieldName)
)
if quotedFieldPrefix != "" {
quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
}
switch m.softTimeOption.SoftTimeType {
case SoftTimeTypeAuto:
switch fieldType {
case LocalTypeDate, LocalTypeDatetime:
return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeBool:
return fmt.Sprintf(`%s=0`, quotedFieldName)
default:
intlog.Errorf(
ctx,
`invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`,
fieldType, fieldName, fieldPrefix,
)
}
case SoftTimeTypeTime:
return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
default:
return fmt.Sprintf(`%s=0`, quotedFieldName)
}
return ""
}
// GetValueByFieldTypeForCreateOrUpdate creates and returns the value for specified field type,
// usually for creating or updating operations.
func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate(
ctx context.Context, fieldType LocalType, isDeletedField bool,
) any {
var value any
if isDeletedField {
switch fieldType {
case LocalTypeDate, LocalTypeDatetime:
value = nil
default:
value = 0
}
return value
}
switch m.softTimeOption.SoftTimeType {
case SoftTimeTypeAuto:
switch fieldType {
case LocalTypeDate, LocalTypeDatetime:
value = gtime.Now()
case LocalTypeInt, LocalTypeUint, LocalTypeInt64:
value = gtime.Timestamp()
case LocalTypeBool:
value = 1
default:
intlog.Errorf(
ctx,
`invalid field type "%s" for soft deleting data`,
fieldType,
)
}
default:
switch fieldType {
case LocalTypeBool:
value = 1
default:
value = m.createValueBySoftTimeOption(isDeletedField)
}
}
return value
}
func (m *softTimeMaintainer) createValueBySoftTimeOption(isDeletedField bool) any {
var value any
if isDeletedField {
switch m.softTimeOption.SoftTimeType {
case SoftTimeTypeTime:
value = nil
default:
value = 0
}
return value
}
switch m.softTimeOption.SoftTimeType {
case SoftTimeTypeTime:
value = gtime.Now()
case SoftTimeTypeTimestamp:
value = gtime.Timestamp()
case SoftTimeTypeTimestampMilli:
value = gtime.TimestampMilli()
case SoftTimeTypeTimestampMicro:
value = gtime.TimestampMicro()
case SoftTimeTypeTimestampNano:
value = gtime.TimestampNano()
default:
panic(gerror.NewCodef(
gcode.CodeInternalPanic,
`unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType,
))
}
return value
}

View File

@ -1,181 +0,0 @@
// 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 (
"fmt"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gutil"
)
var (
createdFieldNames = []string{"created_at", "create_at"} // Default field names of table for automatic-filled created datetime.
updatedFieldNames = []string{"updated_at", "update_at"} // Default field names of table for automatic-filled updated datetime.
deletedFieldNames = []string{"deleted_at", "delete_at"} // Default field names of table for automatic-filled deleted datetime.
)
// Unscoped disables the auto-update time feature for insert, update and delete options.
func (m *Model) Unscoped() *Model {
model := m.getModel()
model.unscoped = true
return model
}
// getSoftFieldNameCreate checks and returns the field name for record creating time.
// If there's no field name for storing creating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameCreated(schema string, table string) string {
// It checks whether this feature disabled.
if m.db.GetConfig().TimeMaintainDisabled {
return ""
}
tableName := ""
if table != "" {
tableName = table
} else {
tableName = m.tablesInit
}
config := m.db.GetConfig()
if config.CreatedAt != "" {
return m.getSoftFieldName(schema, tableName, []string{config.CreatedAt})
}
return m.getSoftFieldName(schema, tableName, createdFieldNames)
}
// getSoftFieldNameUpdate checks and returns the field name for record updating time.
// If there's no field name for storing updating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameUpdated(schema string, table string) (field string) {
// It checks whether this feature disabled.
if m.db.GetConfig().TimeMaintainDisabled {
return ""
}
tableName := ""
if table != "" {
tableName = table
} else {
tableName = m.tablesInit
}
config := m.db.GetConfig()
if config.UpdatedAt != "" {
return m.getSoftFieldName(schema, tableName, []string{config.UpdatedAt})
}
return m.getSoftFieldName(schema, tableName, updatedFieldNames)
}
// getSoftFieldNameDelete checks and returns the field name for record deleting time.
// If there's no field name for storing deleting time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameDeleted(schema string, table string) (field string) {
// It checks whether this feature disabled.
if m.db.GetConfig().TimeMaintainDisabled {
return ""
}
tableName := ""
if table != "" {
tableName = table
} else {
tableName = m.tablesInit
}
config := m.db.GetConfig()
if config.DeletedAt != "" {
return m.getSoftFieldName(schema, tableName, []string{config.DeletedAt})
}
return m.getSoftFieldName(schema, tableName, deletedFieldNames)
}
// getSoftFieldName retrieves and returns the field name of the table for possible key.
func (m *Model) getSoftFieldName(schema string, table string, keys []string) (field string) {
// Ignore the error from TableFields.
fieldsMap, _ := m.TableFields(table, schema)
if len(fieldsMap) > 0 {
for _, key := range keys {
field, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), key,
)
if field != "" {
return
}
}
}
return
}
// getConditionForSoftDeleting retrieves and returns the condition string for soft deleting.
// It supports multiple tables string like:
// "user u, user_detail ud"
// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)"
// "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)"
// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)".
func (m *Model) getConditionForSoftDeleting() string {
if m.unscoped {
return ""
}
conditionArray := garray.NewStrArray()
if gstr.Contains(m.tables, " JOIN ") {
// Base table.
match, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables)
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1]))
// Multiple joined tables, exclude the sub query sql which contains char '(' and ')'.
matches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables)
for _, match := range matches {
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1]))
}
}
if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") {
// Multiple base tables.
for _, s := range gstr.SplitAndTrim(m.tables, ",") {
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(s))
}
}
conditionArray.FilterEmpty()
if conditionArray.Len() > 0 {
return conditionArray.Join(" AND ")
}
// Only one table.
if fieldName := m.getSoftFieldNameDeleted("", m.tablesInit); fieldName != "" {
return fmt.Sprintf(`%s IS NULL`, m.db.GetCore().QuoteWord(fieldName))
}
return ""
}
// getConditionOfTableStringForSoftDeleting does something as its name describes.
// Examples for `s`:
// - `test`.`demo` as b
// - `test`.`demo` b
// - `demo`
// - demo
func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string {
var (
field string
table string
schema string
array1 = gstr.SplitAndTrim(s, " ")
array2 = gstr.SplitAndTrim(array1[0], ".")
)
if len(array2) >= 2 {
table = array2[1]
schema = array2[0]
} else {
table = array2[0]
}
field = m.getSoftFieldNameDeleted(schema, table)
if field == "" {
return ""
}
if len(array1) >= 3 {
return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[2]), m.db.GetCore().QuoteWord(field))
}
if len(array1) >= 2 {
return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[1]), m.db.GetCore().QuoteWord(field))
}
return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(table), m.db.GetCore().QuoteWord(field))
}

View File

@ -16,7 +16,6 @@ import (
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
@ -46,11 +45,14 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
return nil, gerror.NewCode(gcode.CodeMissingParameter, "updating table with empty data")
}
var (
stm = m.softTimeMaintainer()
updateData = m.data
reflectInfo = reflection.OriginTypeAndKind(updateData)
fieldNameUpdate = m.getSoftFieldNameUpdated("", m.tablesInit)
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false)
conditionStr = conditionWhere + conditionExtra
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate(
ctx, "", m.tablesInit,
)
)
if m.unscoped {
fieldNameUpdate = ""
@ -61,7 +63,8 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
var dataMap = anyValueToMapBeforeToRecord(m.data)
// Automatically update the record updating time.
if fieldNameUpdate != "" {
dataMap[fieldNameUpdate] = gtime.Now()
dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false)
dataMap[fieldNameUpdate] = dataValue
}
updateData = dataMap
@ -69,9 +72,10 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
updates := gconv.String(m.data)
// Automatically update the record updating time.
if fieldNameUpdate != "" {
dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false)
if fieldNameUpdate != "" && !gstr.Contains(updates, fieldNameUpdate) {
updates += fmt.Sprintf(`,%s=?`, fieldNameUpdate)
conditionArgs = append([]interface{}{gtime.Now()}, conditionArgs...)
conditionArgs = append([]interface{}{dataValue}, conditionArgs...)
}
}
updateData = updates

View File

@ -20,7 +20,7 @@ import (
type Func func(ctx context.Context) (value interface{}, err error)
const (
DurationNoExpire = 0 // Expire duration that never expires.
DurationNoExpire = time.Duration(0) // Expire duration that never expires.
)
// Default cache object.