xorm/session_update.go
Lunny Xiao cb4f310151
All checks were successful
test mariadb / test mariadb (push) Successful in 5m2s
test cockroach / test cockroach (push) Successful in 7m30s
test mssql / test mssql (push) Successful in 6m10s
test mysql / test mysql (push) Successful in 4m55s
test mysql8 / test mysql8 (push) Successful in 5m16s
test postgres / test postgres (push) Successful in 5m55s
test tidb / test tidb (push) Successful in 5m30s
test sqlite / unit test & test sqlite (push) Successful in 8m37s
Refactor write update (#2310)
Reviewed-on: #2310
2023-07-25 10:49:55 +00:00

320 lines
8.7 KiB
Go

// Copyright 2016 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xorm
import (
"reflect"
"xorm.io/builder"
"xorm.io/xorm/internal/statements"
"xorm.io/xorm/internal/utils"
"xorm.io/xorm/schemas"
)
// enumerated all errors
var (
ErrNoColumnsTobeUpdated = statements.ErrNoColumnsTobeUpdated
)
func (session *Session) genAutoCond(condiBean interface{}) (builder.Cond, error) {
if session.statement.NoAutoCondition {
return builder.NewCond(), nil
}
if c, ok := condiBean.(map[string]interface{}); ok {
eq := make(builder.Eq)
for k, v := range c {
eq[session.engine.Quote(k)] = v
}
if session.statement.RefTable != nil {
if col := session.statement.RefTable.DeletedColumn(); col != nil && !session.statement.GetUnscoped() { // tag "deleted" is enabled
return eq.And(session.statement.CondDeleted(col)), nil
}
}
return eq, nil
}
ct := reflect.TypeOf(condiBean)
k := ct.Kind()
if k == reflect.Ptr {
k = ct.Elem().Kind()
}
if k != reflect.Struct {
return nil, ErrConditionType
}
condTable, err := session.engine.TableInfo(condiBean)
if err != nil {
return nil, err
}
return session.statement.BuildConds(condTable, condiBean, true, true, false, true, false)
}
// Update records, bean's non-empty fields are updated contents,
// condiBean' non-empty filds are conditions
// CAUTION:
//
// 1.bool will defaultly be updated content nor conditions
// You should call UseBool if you have bool to use.
// 2.float32 & float64 may be not inexact as conditions
func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error) {
if session.isAutoClose {
defer session.Close()
}
defer session.resetStatement()
if session.statement.LastError != nil {
return 0, session.statement.LastError
}
v := utils.ReflectValue(bean)
t := v.Type()
// handle before update processors
for _, closure := range session.beforeClosures {
closure(bean)
}
cleanupProcessorsClosures(&session.beforeClosures) // cleanup after used
if processor, ok := interface{}(bean).(BeforeUpdateProcessor); ok {
processor.BeforeUpdate()
}
// --
var colNames []string
var args []interface{}
var err error
isMap := t.Kind() == reflect.Map
isStruct := t.Kind() == reflect.Struct
if isStruct {
if err := session.statement.SetRefBean(bean); err != nil {
return 0, err
}
if len(session.statement.TableName()) == 0 {
return 0, ErrTableNotFound
}
if session.statement.ColumnStr() == "" {
colNames, args, err = session.statement.BuildUpdates(v, false, false,
false, false, true)
} else {
colNames, args, err = session.genUpdateColumns(bean)
}
if err != nil {
return 0, err
}
} else if isMap {
colNames = make([]string, 0)
args = make([]interface{}, 0)
bValue := reflect.Indirect(reflect.ValueOf(bean))
for _, v := range bValue.MapKeys() {
colNames = append(colNames, session.engine.Quote(v.String())+" = ?")
args = append(args, bValue.MapIndex(v).Interface())
}
} else {
return 0, ErrParamsType
}
table := session.statement.RefTable
if session.statement.UseAutoTime && table != nil && table.Updated != "" {
if !session.statement.ColumnMap.Contain(table.Updated) &&
!session.statement.OmitColumnMap.Contain(table.Updated) {
colNames = append(colNames, session.engine.Quote(table.Updated)+" = ?")
col := table.UpdatedColumn()
val, t, err := session.engine.nowTime(col)
if err != nil {
return 0, err
}
if session.engine.dialect.URI().DBType == schemas.ORACLE {
args = append(args, t)
} else {
args = append(args, val)
}
colName := col.Name
if isStruct {
session.afterClosures = append(session.afterClosures, func(bean interface{}) {
col := table.GetColumn(colName)
setColumnTime(bean, col, t)
})
}
}
}
if err = session.statement.ProcessIDParam(); err != nil {
return 0, err
}
var autoCond builder.Cond
if len(condiBean) > 0 {
autoCond, err = session.genAutoCond(condiBean[0])
if err != nil {
return 0, err
}
} else if table != nil {
if col := table.DeletedColumn(); col != nil && !session.statement.GetUnscoped() { // tag "deleted" is enabled
autoCond1 := session.statement.CondDeleted(col)
if autoCond == nil {
autoCond = autoCond1
} else {
autoCond = autoCond.And(autoCond1)
}
}
}
var (
cond = session.statement.Conds().And(autoCond)
doIncVer = isStruct && (table != nil && table.Version != "" && session.statement.CheckVersion)
verValue *reflect.Value
)
if doIncVer {
verValue, err = table.VersionColumn().ValueOfV(&v)
if err != nil {
return 0, err
}
if verValue != nil {
cond = cond.And(builder.Eq{session.engine.Quote(table.Version): verValue.Interface()})
}
}
updateWriter := builder.NewWriter()
if err := session.statement.WriteUpdate(updateWriter, cond, v, colNames, args); err != nil {
return 0, err
}
tableName := session.statement.TableName() // table name must been get before exec because statement will be reset
useCache := session.statement.UseCache
res, err := session.exec(updateWriter.String(), updateWriter.Args()...)
if err != nil {
return 0, err
} else if doIncVer {
if verValue != nil && verValue.IsValid() && verValue.CanSet() {
session.incrVersionFieldValue(verValue)
}
}
if cacher := session.engine.GetCacher(tableName); cacher != nil && useCache {
session.engine.logger.Debugf("[cache] clear table: %v", tableName)
cacher.ClearIds(tableName)
cacher.ClearBeans(tableName)
}
// handle after update processors
if session.isAutoCommit {
for _, closure := range session.afterClosures {
closure(bean)
}
if processor, ok := interface{}(bean).(AfterUpdateProcessor); ok {
session.engine.logger.Debugf("[event] %v has after update processor", tableName)
processor.AfterUpdate()
}
} else {
lenAfterClosures := len(session.afterClosures)
if lenAfterClosures > 0 {
if value, has := session.afterUpdateBeans[bean]; has && value != nil {
*value = append(*value, session.afterClosures...)
} else {
afterClosures := make([]func(interface{}), lenAfterClosures)
copy(afterClosures, session.afterClosures)
// FIXME: if bean is a map type, it will panic because map cannot be as map key
session.afterUpdateBeans[bean] = &afterClosures
}
} else {
if _, ok := interface{}(bean).(AfterUpdateProcessor); ok {
session.afterUpdateBeans[bean] = nil
}
}
}
cleanupProcessorsClosures(&session.afterClosures) // cleanup after used
// --
return res.RowsAffected()
}
func (session *Session) genUpdateColumns(bean interface{}) ([]string, []interface{}, error) {
table := session.statement.RefTable
colNames := make([]string, 0, len(table.ColumnsSeq()))
args := make([]interface{}, 0, len(table.ColumnsSeq()))
for _, col := range table.Columns() {
if !col.IsVersion && !col.IsCreated && !col.IsUpdated {
if session.statement.OmitColumnMap.Contain(col.Name) {
continue
}
}
if col.MapType == schemas.ONLYFROMDB {
continue
}
fieldValuePtr, err := col.ValueOf(bean)
if err != nil {
return nil, nil, err
}
fieldValue := *fieldValuePtr
if col.IsAutoIncrement && utils.IsValueZero(fieldValue) {
continue
}
if (col.IsDeleted && !session.statement.GetUnscoped()) || col.IsCreated {
continue
}
// if only update specify columns
if len(session.statement.ColumnMap) > 0 && !session.statement.ColumnMap.Contain(col.Name) {
continue
}
if session.statement.IncrColumns.IsColExist(col.Name) {
continue
} else if session.statement.DecrColumns.IsColExist(col.Name) {
continue
} else if session.statement.ExprColumns.IsColExist(col.Name) {
continue
}
// !evalphobia! set fieldValue as nil when column is nullable and zero-value
if _, ok := getFlagForColumn(session.statement.NullableMap, col); ok {
if col.Nullable && utils.IsValueZero(fieldValue) {
var nilValue *int
fieldValue = reflect.ValueOf(nilValue)
}
}
if col.IsUpdated && session.statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ {
// if time is non-empty, then set to auto time
val, t, err := session.engine.nowTime(col)
if err != nil {
return nil, nil, err
}
args = append(args, val)
colName := col.Name
session.afterClosures = append(session.afterClosures, func(bean interface{}) {
col := table.GetColumn(colName)
setColumnTime(bean, col, t)
})
} else if col.IsVersion && session.statement.CheckVersion {
args = append(args, 1)
} else {
arg, err := session.statement.Value2Interface(col, fieldValue)
if err != nil {
return colNames, args, err
}
args = append(args, arg)
}
colNames = append(colNames, session.engine.Quote(col.Name)+" = ?")
}
return colNames, args, nil
}