Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions database/gdb/gdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ type Core struct {
config *ConfigNode // Current config node.
localTypeMap *gmap.StrAnyMap // Local type map for database field type conversion.
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
innerMemCache *gcache.Cache // Internal memory cache for storing temporary data.
registry *tableRegistry // Schema metadata registry: table fields, table existence. Replaces innerMemCache.
}

type dynamicConfig struct {
Expand Down Expand Up @@ -710,7 +710,6 @@ const (
defaultMaxIdleConnCount = 10 // Max idle connection count in pool.
defaultMaxOpenConnCount = 0 // Max open connection count in pool. Default is no limit.
defaultMaxConnLifeTime = 30 * time.Second // Max lifetime for per connection in pool in seconds.
cachePrefixTableFields = `TableFields:`
cachePrefixSelectCache = `SelectCache:`
commandEnvKeyForDryRun = "gf.gdb.dryrun"
modelForDaoSuffix = `ForDao`
Expand Down Expand Up @@ -960,14 +959,14 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) {
}
}
c := &Core{
group: group,
debug: gtype.NewBool(),
cache: gcache.New(),
links: gmap.NewKVMapWithChecker[ConfigNode, *sql.DB](linksChecker, true),
logger: glog.New(),
config: node,
localTypeMap: gmap.NewStrAnyMap(true),
innerMemCache: gcache.New(),
group: group,
debug: gtype.NewBool(),
cache: gcache.New(),
links: gmap.NewKVMapWithChecker[ConfigNode, *sql.DB](linksChecker, true),
logger: glog.New(),
config: node,
localTypeMap: gmap.NewStrAnyMap(true),
registry: newTableRegistry(),
dynamicConfig: dynamicConfig{
MaxIdleConnCount: node.MaxIdleConnCount,
MaxOpenConnCount: node.MaxOpenConnCount,
Expand Down
86 changes: 43 additions & 43 deletions database/gdb/gdb_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
Expand Down Expand Up @@ -736,27 +735,28 @@ func (c *Core) writeSqlToLogger(ctx context.Context, sql *Sql) {
}
}

// HasTable determine whether the table name exists in the database.
func (c *Core) HasTable(name string) (bool, error) {
tables, err := c.GetTablesWithCache()
if err != nil {
return false, err
}
// HasTable determines whether the table name exists in the database.
// The optional schema parameter specifies which schema to check; if omitted the default schema for the current database connection is used.
// Lookup is O(1) via the schema registry.
func (c *Core) HasTable(name string, schema ...string) (bool, error) {
schemaName := gutil.GetOrDefaultStr(c.db.GetSchema(), schema...)
charL, charR := c.db.GetChars()
name = gstr.Trim(name, charL+charR)
for _, table := range tables {
if table == name {
return true, nil
}
}
return false, nil
}

// GetInnerMemCache retrieves and returns the inner memory cache object.
func (c *Core) GetInnerMemCache() *gcache.Cache {
return c.innerMemCache
reg := c.db.GetCore().registry
if reg.HasTable(c.db.GetGroup(), schemaName, name) {
return true, nil
}
// Registry not populated yet: fall back to loading all table names from DB.
_, err := c.GetTablesWithCache(schema...)
if err != nil {
return false, err
}
return reg.HasTable(c.db.GetGroup(), schemaName, name), nil
}

// SetTableFields stores pre-built table field metadata into the registry.
// It is used by generated dao code to inject field information at startup.
func (c *Core) SetTableFields(ctx context.Context, table string, fields map[string]*TableField, schema ...string) error {
if table == "" {
return gerror.NewCode(gcode.CodeInvalidParameter, "table name cannot be empty")
Expand All @@ -769,40 +769,40 @@ func (c *Core) SetTableFields(ctx context.Context, table string, fields map[stri
"function TableFields supports only single table operations",
)
}
var (
innerMemCache = c.GetInnerMemCache()
// prefix:group@schema#table
cacheKey = genTableFieldsCacheKey(
c.db.GetGroup(),
gutil.GetOrDefaultStr(c.db.GetSchema(), schema...),
table,
)
c.db.GetCore().registry.Set(
c.db.GetGroup(),
gutil.GetOrDefaultStr(c.db.GetSchema(), schema...),
table,
fields,
)
return innerMemCache.Set(ctx, cacheKey, fields, gcache.DurationNoExpire)
return nil
}

// GetTablesWithCache retrieves and returns the table names of current database with cache.
func (c *Core) GetTablesWithCache() ([]string, error) {
// GetTablesWithCache retrieves and returns the table names for the current database,
// using the registry as a cache. The optional schema parameter specifies which
// schema to query; if omitted the default schema is used.
//
// On first call the DB is queried for all table names and the results are stored
// in the registry as existence markers. Subsequent calls return registry data directly.
func (c *Core) GetTablesWithCache(schema ...string) ([]string, error) {
var (
ctx = c.db.GetCtx()
cacheKey = genTableNamesCacheKey(c.db.GetGroup())
cacheDuration = gcache.DurationNoExpire
innerMemCache = c.GetInnerMemCache()
)
result, err := innerMemCache.GetOrSetFuncLock(
ctx, cacheKey,
func(ctx context.Context) (any, error) {
tableList, err := c.db.Tables(ctx)
if err != nil {
return nil, err
}
return tableList, nil
}, cacheDuration,
group = c.db.GetGroup()
schemaName = gutil.GetOrDefaultStr(c.db.GetSchema(), schema...)
reg = c.db.GetCore().registry
)
// Return from registry if the full table name list has already been loaded for this group+schema.
if tables, loaded := reg.GetLoadedSchemaTables(group, schemaName); loaded {
return tables, nil
}
// Query DB and populate registry as existence markers.
ctx := c.db.GetCtx()
tableList, err := c.db.Tables(ctx, schema...)
if err != nil {
return nil, err
}
return result.Strings(), nil
// Batch register all tables with a single lock acquisition.
reg.Sets(group, schemaName, tableList)
return tableList, nil
}

// IsSoftCreatedFieldName checks and returns whether given field name is an automatic-filled created time.
Expand Down
27 changes: 7 additions & 20 deletions database/gdb/gdb_core_utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,33 +141,22 @@ func (c *Core) TableFields(ctx context.Context, table string, schema ...string)
return
}

// ClearTableFields removes certain cached table fields of current configuration group.
// ClearTableFields clears the cached field data for the specified table so that the
// next call to TableFields re-queries the database. The table's existence marker is
// preserved so that HasTable continues to return true without a DB round-trip.
func (c *Core) ClearTableFields(ctx context.Context, table string, schema ...string) (err error) {
tableFieldsCacheKey := genTableFieldsCacheKey(
c.db.GetCore().registry.Set(
c.db.GetGroup(),
gutil.GetOrDefaultStr(c.db.GetSchema(), schema...),
table,
nil,
)
_, err = c.innerMemCache.Remove(ctx, tableFieldsCacheKey)
return
}

// ClearTableFieldsAll removes all cached table fields of current configuration group.
func (c *Core) ClearTableFieldsAll(ctx context.Context) (err error) {
var (
keys, _ = c.innerMemCache.KeyStrings(ctx)
cachePrefix = cachePrefixTableFields
removedKeys = make([]any, 0)
)
for _, key := range keys {
if gstr.HasPrefix(key, cachePrefix) {
removedKeys = append(removedKeys, key)
}
}

if len(removedKeys) > 0 {
err = c.innerMemCache.Removes(ctx, removedKeys)
}
c.db.GetCore().registry.ClearAll()
return
}

Expand All @@ -194,9 +183,7 @@ func (c *Core) ClearCacheAll(ctx context.Context) (err error) {
if err = c.db.GetCache().Clear(ctx); err != nil {
return err
}
if err = c.GetInnerMemCache().Clear(ctx); err != nil {
return err
}
c.db.GetCore().registry.ClearAll()
return
}

Expand Down
26 changes: 5 additions & 21 deletions database/gdb/gdb_driver_wrapper_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import (
"database/sql"
"fmt"

"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/encoding/gjson"
"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/text/gstr"
"github.com/gogf/gf/v2/util/gutil"
)
Expand Down Expand Up @@ -70,31 +68,17 @@ func (d *DriverWrapperDB) TableFields(
)
}
var (
innerMemCache = d.GetCore().GetInnerMemCache()
// prefix:group@schema#table
cacheKey = genTableFieldsCacheKey(
d.GetGroup(),
gutil.GetOrDefaultStr(d.GetSchema(), schema...),
table,
)
cacheFunc = func(ctx context.Context) (any, error) {
reg = d.GetCore().registry
group = d.GetGroup()
sName = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
loader = func() (map[string]*TableField, error) {
return d.DB.TableFields(
context.WithValue(ctx, ctxKeyInternalProducedSQL, struct{}{}),
table, schema...,
)
}
value *gvar.Var
)
value, err = innerMemCache.GetOrSetFuncLock(
ctx, cacheKey, cacheFunc, gcache.DurationNoExpire,
)
if err != nil {
return
}
if !value.IsNil() {
fields = value.Val().(map[string]*TableField)
}
return
return reg.GetOrSet(group, sName, table, loader)
}

// DoInsert inserts or updates data for given table.
Expand Down
23 changes: 1 addition & 22 deletions database/gdb/gdb_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n
)
// If `Prefix` is given, it checks and retrieves the table name.
if in.Prefix != "" {
hasTable, _ := db.GetCore().HasTable(in.Prefix)
hasTable, _ := db.GetCore().HasTable(in.Prefix, in.Schema)
if hasTable {
in.Table = in.Prefix
} else {
Expand Down Expand Up @@ -981,17 +981,6 @@ func FormatMultiLineSqlToSingle(sql string) (string, error) {
return sql, nil
}

// genTableFieldsCacheKey generates cache key for table fields.
func genTableFieldsCacheKey(group, schema, table string) string {
return fmt.Sprintf(
`%s%s@%s#%s`,
cachePrefixTableFields,
group,
schema,
table,
)
}

// genSelectCacheKey generates cache key for select.
func genSelectCacheKey(table, group, schema, name, sql string, args ...any) string {
if name == "" {
Expand All @@ -1005,13 +994,3 @@ func genSelectCacheKey(table, group, schema, name, sql string, args ...any) stri
}
return fmt.Sprintf(`%s%s`, cachePrefixSelectCache, name)
}

// genTableNamesCacheKey generates cache key for table names.
func genTableNamesCacheKey(group string) string {
return fmt.Sprintf(`Tables:%s`, group)
}

// genSoftTimeFieldNameTypeCacheKey generates cache key for soft time field name and type.
func genSoftTimeFieldNameTypeCacheKey(schema, table string, candidateFields []string) string {
return fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%s`, schema, table, strings.Join(candidateFields, "_"))
}
59 changes: 23 additions & 36 deletions database/gdb/gdb_model_soft_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/internal/utils"
"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"
Expand Down Expand Up @@ -66,12 +65,6 @@ type iSoftTimeMaintainer interface {
GetDeleteData(ctx context.Context, prefix, fieldName string, localType LocalType) (holder string, value any)
}

// 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"}
Expand Down Expand Up @@ -144,40 +137,34 @@ func (m *softTimeMaintainer) GetFieldInfo(
}

// getSoftFieldNameAndType retrieves and returns the field name of the table for possible key.
// It derives the result directly from the table's field map (already cached in the registry)
// instead of maintaining a separate cache layer, which eliminates:
// - cross-group cache pollution (different database groups with same table name)
// - cache inconsistency when clearing table fields
// - concurrent cache penetration during cold start
func (m *softTimeMaintainer) getSoftFieldNameAndType(
ctx context.Context, schema, table string, candidateFields []string,
) (fieldName string, fieldType LocalType) {
// Build cache key
cacheKey := genSoftTimeFieldNameTypeCacheKey(schema, table, candidateFields)

// Try to get from cache
cache := m.db.GetCore().GetInnerMemCache()
result, err := cache.GetOrSetFunc(ctx, cacheKey, func(ctx context.Context) (any, error) {
// Get table fields
fieldsMap, err := m.TableFields(table, schema)
if err != nil || len(fieldsMap) == 0 {
return nil, err
}

// Search for matching field
for _, field := range candidateFields {
if name := searchFieldNameFromMap(fieldsMap, field); name != "" {
fType, _ := m.db.CheckLocalTypeForField(ctx, fieldsMap[name].Type, nil)
return getSoftFieldNameAndTypeCacheItem{
FieldName: name,
FieldType: fType,
}, nil
}
}
return nil, nil
}, gcache.DurationNoExpire)

if err != nil || result == nil {
// Call chain to registry cache:
// m.TableFields(table, schema)
// → Model.TableFields
// → m.db.TableFields(ctx, table, schema)
// → DriverWrapperDB.TableFields
// → reg.GetOrSet(group, schema, table, loader)
// → tableRegistry.GetOrSet
// [cache hit] → return cached fields (O(1) map lookup)
// [cache miss] → loader() queries DB + stores in registry (double-checked locking)
fieldsMap, err := m.TableFields(table, schema)
if err != nil || len(fieldsMap) == 0 {
return "", LocalTypeUndefined
}

item := result.Val().(getSoftFieldNameAndTypeCacheItem)
return item.FieldName, item.FieldType
for _, field := range candidateFields {
if name := searchFieldNameFromMap(fieldsMap, field); name != "" {
fType, _ := m.db.CheckLocalTypeForField(ctx, fieldsMap[name].Type, nil)
return name, fType
}
}
return "", LocalTypeUndefined
}

func searchFieldNameFromMap(fieldsMap map[string]*TableField, key string) string {
Expand Down
2 changes: 1 addition & 1 deletion database/gdb/gdb_model_utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (m *Model) getModel() *Model {
func (m *Model) mappingAndFilterToTableFields(table string, fields []any, filter bool) []any {
var fieldsTable = table
if fieldsTable != "" {
hasTable, _ := m.db.GetCore().HasTable(fieldsTable)
hasTable, _ := m.db.GetCore().HasTable(fieldsTable, m.schema)
if !hasTable {
if fieldsTable != m.tablesInit {
// Table/alias unknown (e.g., FieldsPrefix called before LeftJoin), skip filtering.
Expand Down
Loading
Loading