Files
gf/database/gdb/gdb_model.go

376 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/v3/os/gctx"
"github.com/gogf/gf/v3/text/gregex"
"github.com/gogf/gf/v3/text/gstr"
"github.com/gogf/gf/v3/util/gconv"
)
// 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 []any // Operation fields, multiple fields joined using char ','.
fieldsEx []any // Excluded operation fields, it here uses slice instead of string type for quick filtering.
withArray []any // Arguments for With feature.
withAll bool // Enable model association operations on all objects that have "with" tag in the struct.
extraArgs []any // 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 []any // 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 any // 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.
onDuplicate any // onDuplicate is used for on Upsert clause.
onDuplicateEx any // onDuplicateEx is used for excluding some columns on Upsert clause.
onConflict any // onConflict is used for conflict keys on Upsert clause.
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.
shardingConfig ShardingConfig // ShardingConfig for database/table sharding feature.
shardingValue any // Sharding value for sharding feature.
handlers []ModelHandler // Chaining handlers for model operations.
handlerIndex int
}
// ModelHandler is a function that handles given Model and returns a new Model that is custom modified.
type ModelHandler func(ctx context.Context, model *Model) *Model
// ChunkHandler is a function that is used in function Chunk, which handles given Result and error.
// It returns true if it wants to continue chunking, or else it returns false to stop chunking.
type ChunkHandler func(result Result, err error) bool
const (
linkTypeMaster = 1
linkTypeSlave = 2
defaultField = "*"
whereHolderOperatorWhere = 1
whereHolderOperatorAnd = 2
whereHolderOperatorOr = 3
whereHolderTypeDefault = "Default"
whereHolderTypeNoArgs = "NoArgs"
whereHolderTypeIn = "In"
// inCallHandlersKey is the context key for in call handlers to avoid recursively calling.
inCallHandlersKey = gctx.StrKey("InCallHandler")
)
func newEmptyModel(db DB, schema string) *Model {
newModel := &Model{
db: db,
schema: schema,
start: -1,
offset: -1,
filter: true,
tableAliasMap: make(map[string]string),
}
newModel.whereBuilder = newModel.Builder()
return newModel
}
// Model creates and returns a new ORM model from given schema.
// The parameter `tableNameQueryOrStruct` can be more than one table names, and also alias name, like:
// 1. Model names:
// db.Model("user")
// db.Model("user u")
// db.Model("user, user_detail")
// db.Model("user u, user_detail ud")
// 2. Model name with alias:
// db.Model("user", "u")
// 3. Model name with sub-query:
// db.Model("? AS a, ? AS b", subQuery1, subQuery2)
func (c *Core) Model(tableNameQueryOrStruct ...any) *Model {
newModel := newEmptyModel(c.db, c.schema)
return newModel.Handler(func(ctx context.Context, model *Model) *Model {
var (
tableStr string
tableName string
extraArgs []any
)
// Model creation with sub-query.
if len(tableNameQueryOrStruct) > 1 {
conditionStr := gconv.String(tableNameQueryOrStruct[0])
if gstr.Contains(conditionStr, "?") {
whereHolder := WhereHolder{
Where: conditionStr,
Args: tableNameQueryOrStruct[1:],
}
tableStr, extraArgs = formatWhereHolder(ctx, c.db, formatWhereHolderInput{
WhereHolder: whereHolder,
OmitNil: false,
OmitEmpty: false,
Schema: "",
Table: "",
})
}
}
// Normal model creation.
if tableStr == "" {
tableNames := make([]string, len(tableNameQueryOrStruct))
for k, v := range tableNameQueryOrStruct {
if s, ok := v.(string); ok {
tableNames[k] = s
} else if tableName = getTableNameFromOrmTag(v); tableName != "" {
tableNames[k] = tableName
}
}
if len(tableNames) > 1 {
tableStr = fmt.Sprintf(
`%s AS %s`, c.QuotePrefixTableName(tableNames[0]), c.QuoteWord(tableNames[1]),
)
} else if len(tableNames) == 1 {
tableStr = c.QuotePrefixTableName(tableNames[0])
}
}
model.tables = tableStr
model.tablesInit = tableStr
model.extraArgs = extraArgs
return model
})
}
// Clone creates and returns a new model which is a Clone of current model.
// It uses chaining handlers design for high performance of model clone.
func (m *Model) Clone() *Model {
newModel := newEmptyModel(m.db, m.schema)
newModel.handlers = make([]ModelHandler, len(m.handlers))
if len(newModel.handlers) > 0 {
copy(newModel.handlers, m.handlers)
}
return newModel
}
// Raw creates and returns a model based on a raw sql not a table.
// Example:
//
// db.Raw("SELECT * FROM `user` WHERE `name` = ?", "john").Scan(&result)
func (c *Core) Raw(rawSql string, args ...any) *Model {
newModel := c.Model()
return newModel.Handler(func(ctx context.Context, model *Model) *Model {
model.rawSql = rawSql
model.extraArgs = args
return model
})
}
// Raw sets current model as a raw sql model.
//
// Example:
//
// db.Raw("SELECT * FROM `user` WHERE `name` = ?", "john").Scan(&result)
//
// See Core.Raw.
func (m *Model) Raw(rawSql string, args ...any) *Model {
newModel := m.db.Raw(rawSql, args...)
return newModel.Handler(func(ctx context.Context, model *Model) *Model {
model.db = m.db
model.tx = m.tx
return model
})
}
// Raw creates and returns a model based on a raw sql not a table.
func (tx *TXCore) Raw(rawSql string, args ...any) *Model {
return tx.Model().Raw(rawSql, args...)
}
// With creates and returns an ORM model based on metadata of given object.
func (c *Core) With(objects ...any) *Model {
return c.db.Model().With(objects...)
}
// Partition sets Partition name.
//
// Example:
// dao.User.Partition"p1","p2","p3").All()
func (m *Model) Partition(partitions ...string) *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.partition = gstr.Join(partitions, ",")
return model
})
}
// Model acts like Core.Model except it operates on transaction.
// See Core.Model.
func (tx *TXCore) Model(tableNameQueryOrStruct ...any) *Model {
newModel := tx.db.Model(tableNameQueryOrStruct...)
return newModel.Handler(func(ctx context.Context, model *Model) *Model {
model.db = tx.db
model.tx = tx
return model
})
}
// With acts like Core.With except it operates on transaction.
// See Core.With.
func (tx *TXCore) With(object any) *Model {
return tx.Model().With(object)
}
// As sets an alias name for current table.
func (m *Model) As(as string) *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
if model.tables == "" {
return model
}
split := " JOIN "
if gstr.ContainsI(model.tables, split) {
// For join table.
array := gstr.Split(model.tables, split)
array[len(array)-1], _ = gregex.ReplaceString(
`(.+) ON`,
fmt.Sprintf(`$1 AS %s ON`, as), array[len(array)-1],
)
model.tables = gstr.Join(array, split)
} else {
// For base table.
model.tables = gstr.TrimRight(model.tables) + " AS " + as
}
return model
})
}
// DB sets/changes the db object for current operation.
func (m *Model) DB(db DB) *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.db = db
return model
})
}
// TX sets/changes the transaction for current operation.
func (m *Model) TX(tx TX) *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.db = tx.GetDB()
model.tx = tx
return model
})
}
// Schema sets the schema for current operation.
func (m *Model) Schema(schema string) *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.schema = schema
return model
})
}
// Master marks the following operation on master node.
func (m *Model) Master() *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.linkType = linkTypeMaster
return model
})
}
// Slave marks the following operation on slave node.
// Note that it makes sense only if there's any slave node configured.
func (m *Model) Slave() *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.linkType = linkTypeSlave
return model
})
}
// Args sets custom arguments for model operation.
func (m *Model) Args(args ...any) *Model {
return m.Handler(func(ctx context.Context, model *Model) *Model {
model.extraArgs = append(model.extraArgs, args)
return model
})
}
// Handler calls each of `handlers` on current Model and returns a new Model.
// ModelHandler is a function that handles given Model and returns a new Model that is custom modified.
func (m *Model) Handler(handlers ...ModelHandler) *Model {
m.handlers = append(m.handlers, handlers...)
return m
}
// callHandlers executes all handlers for current Model.
func (m *Model) callHandlers(ctx context.Context) *Model {
// pointer address comparison.
if ctx.Value(inCallHandlersKey) == m {
return m
}
ctx = context.WithValue(ctx, inCallHandlersKey, m)
if len(m.handlers) == 0 {
return m
}
var (
model = m
oldHandlersLength = len(model.handlers)
newHandlersLength = oldHandlersLength
)
for {
// Exit the loop if all handlers have been processed
if model.handlerIndex >= len(model.handlers) {
break
}
// Record the current length of handlers
oldHandlersLength = len(model.handlers)
// Execute the current handler
model = model.handlers[model.handlerIndex](ctx, model)
// Check if new handlers were added
newHandlersLength = len(model.handlers)
if newHandlersLength > oldHandlersLength {
var (
addedCount = newHandlersLength - oldHandlersLength
targetPos = model.handlerIndex + 1
)
// Insert newly added handlers into the target position using element swapping technique
// Example of the swapping logic:
// 1. We have an array of digits: 123456
// 2. We're at position 2 and add two new digits 7,8 at the end: 12345678
// 3. We want to insert these new digits after position 2, so we perform these swaps:
// - Swap 3 and 7: 12745638
// - Swap 4 and 8: 12785634
// - Swap 5 and 3: 12783654
// - Swap 6 and 4: 12783456
// 4. Result: 12783456 - new elements are inserted after position 2
for i := 0; i < addedCount; i++ {
// Start from each new element and swap it with preceding elements until it reaches target position
newItemPos := oldHandlersLength + i
for j := newItemPos; j > targetPos+i; j-- {
// Swap elements at positions j and j-1
model.handlers[j], model.handlers[j-1] = model.handlers[j-1], model.handlers[j]
}
}
}
// Move to the next handler
model.handlerIndex++
}
return model
}