Files
gf/database/gdb/gdb_model.go
Lance Add df463d75bc fix(database/gdb): Resolve the cache error overwriting caused by the use of fixed cache keys in pagination queries. (#4339)
```golang
func main() {
	adapter := gcache.NewAdapterRedis(g.Redis())
	g.DB().GetCache().SetAdapter(adapter)
	result, count, err := g.Model("TBL_USER").Cache(gdb.CacheOption{
		Duration: 100 * time.Minute,
		Name:     "VIP",
	}).AllAndCount(false)
	g.DumpJson(result)
	fmt.Println(count, err)
}
```
执行这段查询后`g.DumpJson(result)`的结果是`[
    {
        "COUNT(1)": 5
    }

]`,但是正确结果应该是五条用户信息,查看源代码后发现先执行的count查询和后来select查询都是直接使用了`VIP`这个缓存key,在redis中实际缓存key是`SelectCache:VIP`,第二步查询select获得的是count查询的缓存,所以查询结果是错的。
因此为`Model`增加一个`PageCache`方法允许用户分别设置`count query`和`data query`的缓存参数
```golang
// PageCache sets the cache feature for pagination queries. It allows to configure
// separate cache options for count query and data query in pagination.
//
// Note that, the cache feature is disabled if the model is performing select statement
// on a transaction.
func (m *Model) PageCache(countOption CacheOption, dataOption CacheOption) *Model {
	model := m.getModel()
	model.pageCacheOption = []CacheOption{countOption, dataOption}
	model.cacheEnabled = true
	return model
}
```
然后`AllAndCount`在查询时分别给两个查询设置对应的缓存参数`ScanAndCount`同理
```golang

// AllAndCount retrieves all records and the total count of records from the model.
// If useFieldForCount is true, it will use the fields specified in the model for counting;
// otherwise, it will use a constant value of 1 for counting.
// It returns the result as a slice of records, the total count of records, and an error if any.
// The where parameter is an optional list of conditions to use when retrieving records.
//
// Example:
//
//	var model Model
//	var result Result
//	var count int
//	where := []any{"name = ?", "John"}
//	result, count, err := model.AllAndCount(true)
//	if err != nil {
//	    // Handle error.
//	}
//	fmt.Println(result, count)
func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount int, err error) {
	// Clone the model for counting
	countModel := m.Clone()

	// If useFieldForCount is false, set the fields to a constant value of 1 for counting
	if !useFieldForCount {
		countModel.fields = []any{Raw("1")}
	}
	if len(m.pageCacheOption) > 0 {
		countModel = countModel.Cache(m.pageCacheOption[0])
	}

	// Get the total count of records
	totalCount, err = countModel.Count()
	if err != nil {
		return
	}

	// If the total count is 0, there are no records to retrieve, so return early
	if totalCount == 0 {
		return
	}

	resultModel := m.Clone()
	if len(m.pageCacheOption) > 1 {
		resultModel = resultModel.Cache(m.pageCacheOption[1])
	}

	// Retrieve all records
	result, err = resultModel.doGetAll(m.GetCtx(), SelectTypeDefault, false)
	return
}
```

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-01-16 10:33:05 +08:00

351 lines
12 KiB
Go
Raw Permalink 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/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/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.
pageCacheOption []CacheOption // Cache option for paging 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 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.
}
// ModelHandler is a function that handles given Model and returns a new Model that is custom modified.
type ModelHandler func(m *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"
)
// 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 {
var (
ctx = c.db.GetCtx()
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])
}
}
m := &Model{
db: c.db,
schema: c.schema,
tablesInit: tableStr,
tables: tableStr,
start: -1,
offset: -1,
filter: true,
extraArgs: extraArgs,
tableAliasMap: make(map[string]string),
}
m.whereBuilder = m.Builder()
if defaultModelSafe {
m.safe = true
}
return m
}
// 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 {
model := c.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 {
model := m.db.Raw(rawSql, args...)
model.db = m.db
model.tx = m.tx
return model
}
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.Ctx(ctx).Partition"p1","p2","p3").All()
func (m *Model) Partition(partitions ...string) *Model {
model := m.getModel()
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 {
model := tx.db.Model(tableNameQueryOrStruct...)
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)
}
// Ctx sets the context for current operation.
func (m *Model) Ctx(ctx context.Context) *Model {
if ctx == nil {
return m
}
model := m.getModel()
model.db = model.db.Ctx(ctx)
if m.tx != nil {
model.tx = model.tx.Ctx(ctx)
}
return model
}
// GetCtx returns the context for current Model.
// It returns `context.Background()` is there's no context previously set.
func (m *Model) GetCtx() context.Context {
if m.tx != nil && m.tx.GetCtx() != nil {
return m.tx.GetCtx()
}
return m.db.GetCtx()
}
// As sets an alias name for current table.
func (m *Model) As(as string) *Model {
if m.tables != "" {
model := m.getModel()
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
}
return m
}
// DB sets/changes the db object for current operation.
func (m *Model) DB(db DB) *Model {
model := m.getModel()
model.db = db
return model
}
// TX sets/changes the transaction for current operation.
func (m *Model) TX(tx TX) *Model {
model := m.getModel()
model.db = tx.GetDB()
model.tx = tx
return model
}
// Schema sets the schema for current operation.
func (m *Model) Schema(schema string) *Model {
model := m.getModel()
model.schema = schema
return model
}
// Clone creates and returns a new model which is a Clone of current model.
// Note that it uses deep-copy for the Clone.
func (m *Model) Clone() *Model {
newModel := (*Model)(nil)
if m.tx != nil {
newModel = m.tx.Model(m.tablesInit)
} else {
newModel = m.db.Model(m.tablesInit)
}
// Basic attributes copy.
*newModel = *m
// WhereBuilder copy, note the attribute pointer.
newModel.whereBuilder = m.whereBuilder.Clone()
newModel.whereBuilder.model = newModel
// Shallow copy slice attributes.
if n := len(m.fields); n > 0 {
newModel.fields = make([]any, n)
copy(newModel.fields, m.fields)
}
if n := len(m.fieldsEx); n > 0 {
newModel.fieldsEx = make([]any, n)
copy(newModel.fieldsEx, m.fieldsEx)
}
if n := len(m.extraArgs); n > 0 {
newModel.extraArgs = make([]any, n)
copy(newModel.extraArgs, m.extraArgs)
}
if n := len(m.withArray); n > 0 {
newModel.withArray = make([]any, n)
copy(newModel.withArray, m.withArray)
}
if n := len(m.having); n > 0 {
newModel.having = make([]any, n)
copy(newModel.having, m.having)
}
return newModel
}
// Master marks the following operation on master node.
func (m *Model) Master() *Model {
model := m.getModel()
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 {
model := m.getModel()
model.linkType = linkTypeSlave
return model
}
// Safe marks this model safe or unsafe. If safe is true, it clones and returns a new model object
// whenever the operation done, or else it changes the attribute of current model.
func (m *Model) Safe(safe ...bool) *Model {
if len(safe) > 0 {
m.safe = safe[0]
} else {
m.safe = true
}
return m
}
// Args sets custom arguments for model operation.
func (m *Model) Args(args ...any) *Model {
model := m.getModel()
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 {
model := m.getModel()
for _, handler := range handlers {
model = handler(model)
}
return model
}