From 019305126d47c8a9b137a5efb05e4c9c7f7a915c Mon Sep 17 00:00:00 2001 From: lanceadd <1196661499@qq.com> Date: Tue, 24 Feb 2026 16:23:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(database):=20=E4=BD=BF=E7=94=A8tab?= =?UTF-8?q?leRegistry=E6=9B=BF=E4=BB=A3innerMemCache=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=A1=A8=E7=BB=93=E6=9E=84=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将Core结构体中的innerMemCache替换为tableRegistry以统一管理表结构元数据 - HasTable函数优化为通过tableRegistry完成表存在性判断,提高查询效率 - SetTableFields改为通过registry缓存表字段信息,移除直接操作缓存的逻辑 - GetTablesWithCache改用registry缓存表名列表以避免重复数据库查询 - ClearTableFields及ClearTableFieldsAll改为操作registry,实现缓存清理 - TableFields获取及软时间字段缓存逻辑改为访问registry,去除冗余缓存层 - 删除与innerMemCache相关的缓存键生成及无用缓存代码 - 新增tableRegistry类型,实现线程安全的三维(group-schema-table)元数据缓存管理 - 修改相关调用逻辑以适配registry缓存体系,提升缓存一致性及并发性能 --- database/gdb/gdb.go | 19 ++- database/gdb/gdb_core.go | 86 +++++------ database/gdb/gdb_core_utility.go | 26 +--- database/gdb/gdb_driver_wrapper_db.go | 26 +--- database/gdb/gdb_func.go | 21 --- database/gdb/gdb_model_soft_time.go | 59 +++----- database/gdb/gdb_model_utility.go | 2 +- database/gdb/gdb_schema_table_registry.go | 168 ++++++++++++++++++++++ 8 files changed, 255 insertions(+), 152 deletions(-) create mode 100644 database/gdb/gdb_schema_table_registry.go diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index cc99d8e5a3d..f638495a6cc 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -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 { @@ -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` @@ -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, diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index 4b23af05deb..bacf0585709 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -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" @@ -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") @@ -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 we already have any tables registered for this group+schema. + if tables := reg.Tables(group, schemaName); len(tables) > 0 { + 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. diff --git a/database/gdb/gdb_core_utility.go b/database/gdb/gdb_core_utility.go index b97d7431e85..fc0a04f01f5 100644 --- a/database/gdb/gdb_core_utility.go +++ b/database/gdb/gdb_core_utility.go @@ -141,33 +141,21 @@ func (c *Core) TableFields(ctx context.Context, table string, schema ...string) return } -// ClearTableFields removes certain cached table fields of current configuration group. +// ClearTableFields removes the cached fields for the specified table. +// This clears ALL schema metadata for that table (fields, soft-delete field derivations) +// since the registry is the single source of truth for all schema metadata. func (c *Core) ClearTableFields(ctx context.Context, table string, schema ...string) (err error) { - tableFieldsCacheKey := genTableFieldsCacheKey( + c.db.GetCore().registry.Delete( c.db.GetGroup(), gutil.GetOrDefaultStr(c.db.GetSchema(), schema...), table, ) - _, 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 } @@ -194,9 +182,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 } diff --git a/database/gdb/gdb_driver_wrapper_db.go b/database/gdb/gdb_driver_wrapper_db.go index 81c5b729c40..239fde827ff 100644 --- a/database/gdb/gdb_driver_wrapper_db.go +++ b/database/gdb/gdb_driver_wrapper_db.go @@ -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" ) @@ -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. diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 437c981f8fe..c75bfca6cd5 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -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 == "" { @@ -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, "_")) -} diff --git a/database/gdb/gdb_model_soft_time.go b/database/gdb/gdb_model_soft_time.go index 4deb39620ff..bd922143bee 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -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" @@ -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"} @@ -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 { diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index 1ced41c39b4..0a9a3f6570d 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -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. diff --git a/database/gdb/gdb_schema_table_registry.go b/database/gdb/gdb_schema_table_registry.go new file mode 100644 index 00000000000..0c38bde6280 --- /dev/null +++ b/database/gdb/gdb_schema_table_registry.go @@ -0,0 +1,168 @@ +// 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 "sync" + +// tableRegistryKey identifies a table within a specific database group and schema. +// All three dimensions are required to avoid any cross-schema or cross-group confusion. +type tableRegistryKey struct { + group string + schema string + table string +} + +// tableRegistry is the single source of truth for all schema metadata. +// It replaces innerMemCache for table fields and table name lookups. +// +// Addressing is 3D: (group, schema, table), which eliminates schema-confusion bugs +// that existed when different schemas or database groups used the same cache key. +// +// Map value semantics: +// - key absent: table not registered +// - nil value: table registered as an existence marker (fields not yet loaded) +// - non-nil: table fields have been loaded from the database +type tableRegistry struct { + mu sync.RWMutex + data map[tableRegistryKey]map[string]*TableField +} + +func newTableRegistry() *tableRegistry { + return &tableRegistry{ + data: make(map[tableRegistryKey]map[string]*TableField), + } +} + +// Get retrieves the field map for the given table. +// Returns (fields, true) if the key exists (fields may still be nil for existence-only entries). +// Returns (nil, false) if the key is not present. +func (r *tableRegistry) Get(group, schema, table string) (map[string]*TableField, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + fields, ok := r.data[tableRegistryKey{group, schema, table}] + return fields, ok +} + +// Set stores field data for the specified table, overwriting any existing entry. +// Passing nil fields registers the table as an existence marker without field data. +func (r *tableRegistry) Set(group, schema, table string, fields map[string]*TableField) { + r.mu.Lock() + defer r.mu.Unlock() + r.data[tableRegistryKey{group, schema, table}] = fields +} + +// SetIfNotExist marks the table as known without loading its fields. +// If the table is already registered (with or without fields), this is a no-op. +// It returns true if the table was newly registered, false if it already existed. +func (r *tableRegistry) SetIfNotExist(group, schema, table string) bool { + r.mu.Lock() + defer r.mu.Unlock() + key := tableRegistryKey{group, schema, table} + if _, ok := r.data[key]; !ok { + r.data[key] = nil + return true + } + return false +} + +// Sets marks multiple tables as known without loading their fields. +// This is more efficient than calling SetIfNotExist in a loop as it acquires the lock only once. +// If a table is already registered (with or without fields), it is skipped. +func (r *tableRegistry) Sets(group, schema string, tables []string) { + r.mu.Lock() + defer r.mu.Unlock() + for _, table := range tables { + key := tableRegistryKey{group, schema, table} + if _, ok := r.data[key]; !ok { + r.data[key] = nil + } + } +} + +// LockFunc locks writing with given callback function `f` within RWMutex.Lock. +// This allows batch operations on the registry with a single lock acquisition. +func (r *tableRegistry) LockFunc(f func(data map[tableRegistryKey]map[string]*TableField)) { + r.mu.Lock() + defer r.mu.Unlock() + f(r.data) +} + +// HasTable reports whether the specified table is registered (O(1)). +func (r *tableRegistry) HasTable(group, schema, table string) bool { + r.mu.RLock() + defer r.mu.RUnlock() + _, ok := r.data[tableRegistryKey{group, schema, table}] + return ok +} + +// Tables returns all registered table names for the given group and schema. +func (r *tableRegistry) Tables(group, schema string) []string { + r.mu.RLock() + defer r.mu.RUnlock() + var tables []string + for key := range r.data { + if key.group == group && key.schema == schema { + tables = append(tables, key.table) + } + } + return tables +} + +// GetOrSet returns field data for the specified table, invoking loader to populate +// the registry on a cache miss. Uses double-checked locking so that: +// - concurrent reads on already-loaded tables never block each other (RLock), +// - only one goroutine executes loader per table on cold start (Lock), +// - unrelated tables' reads are not affected after the lock is released. +// +// A nil return from loader is stored as an empty map so that subsequent calls +// do not re-invoke loader (distinguishes "loaded with no fields" from "not loaded"). +func (r *tableRegistry) GetOrSet( + group, schema, table string, + loader func() (map[string]*TableField, error), +) (map[string]*TableField, error) { + // Fast path: fields already loaded. + r.mu.RLock() + entry, ok := r.data[tableRegistryKey{group, schema, table}] + r.mu.RUnlock() + if ok && entry != nil { + return entry, nil + } + + // Slow path: acquire write lock and load. + r.mu.Lock() + defer r.mu.Unlock() + + // Double-check under write lock. + entry, ok = r.data[tableRegistryKey{group, schema, table}] + if ok && entry != nil { + return entry, nil + } + + fields, err := loader() + if err != nil { + return nil, err + } + if fields == nil { + fields = make(map[string]*TableField) // empty map marks "loaded, no fields found" + } + r.data[tableRegistryKey{group, schema, table}] = fields + return fields, nil +} + +// Delete removes the registry entry for the specified table. +func (r *tableRegistry) Delete(group, schema, table string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.data, tableRegistryKey{group, schema, table}) +} + +// ClearAll removes every entry from the registry. +func (r *tableRegistry) ClearAll() { + r.mu.Lock() + defer r.mu.Unlock() + r.data = make(map[tableRegistryKey]map[string]*TableField) +} From 7be1999087bbfaceacaf3d6de7a85012f7b45464 Mon Sep 17 00:00:00 2001 From: lanceadd <1196661499@qq.com> Date: Wed, 25 Feb 2026 10:32:28 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(database):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=A1=A8=E6=B3=A8=E5=86=8C=E8=A1=A8?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 GetLoadedSchemaTables 方法以跟踪完整表名列表是否已从数据库加载 - 添加 schemaKey 类型用于标识 (group, schema) 对以跟踪表列表加载状态 - 新增 loadedSchemas 映射跟踪哪些 (group, schema) 对的完整表名列表已加载 - 更新 Sets 方法记录完整表名列表已从数据库加载的信息 - 修改 ClearTableFields 方法保留表存在标记以避免数据库往返查询 - 更新 ClearAll 方法清除已加载模式标记确保下次访问重新查询数据库 - 在 gdb_func.go 中为 HasTable 调用添加模式参数支持 --- database/gdb/gdb_core.go | 4 +- database/gdb/gdb_core_utility.go | 9 ++-- database/gdb/gdb_func.go | 2 +- database/gdb/gdb_schema_table_registry.go | 63 ++++++++++++++--------- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index bacf0585709..436b90be45f 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -790,8 +790,8 @@ func (c *Core) GetTablesWithCache(schema ...string) ([]string, error) { schemaName = gutil.GetOrDefaultStr(c.db.GetSchema(), schema...) reg = c.db.GetCore().registry ) - // Return from registry if we already have any tables registered for this group+schema. - if tables := reg.Tables(group, schemaName); len(tables) > 0 { + // 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. diff --git a/database/gdb/gdb_core_utility.go b/database/gdb/gdb_core_utility.go index fc0a04f01f5..b2218f47d9f 100644 --- a/database/gdb/gdb_core_utility.go +++ b/database/gdb/gdb_core_utility.go @@ -141,14 +141,15 @@ func (c *Core) TableFields(ctx context.Context, table string, schema ...string) return } -// ClearTableFields removes the cached fields for the specified table. -// This clears ALL schema metadata for that table (fields, soft-delete field derivations) -// since the registry is the single source of truth for all schema metadata. +// 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) { - c.db.GetCore().registry.Delete( + c.db.GetCore().registry.Set( c.db.GetGroup(), gutil.GetOrDefaultStr(c.db.GetSchema(), schema...), table, + nil, ) return } diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index c75bfca6cd5..ad0d449186d 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -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 { diff --git a/database/gdb/gdb_schema_table_registry.go b/database/gdb/gdb_schema_table_registry.go index 0c38bde6280..6d338fa58c9 100644 --- a/database/gdb/gdb_schema_table_registry.go +++ b/database/gdb/gdb_schema_table_registry.go @@ -8,32 +8,36 @@ package gdb import "sync" -// tableRegistryKey identifies a table within a specific database group and schema. -// All three dimensions are required to avoid any cross-schema or cross-group confusion. +// tableRegistryKey identifies a table within database group and schema. type tableRegistryKey struct { group string schema string table string } +// schemaKey identifies a (group, schema) pair for tracking loaded table lists. +type schemaKey struct { + group string + schema string +} + // tableRegistry is the single source of truth for all schema metadata. -// It replaces innerMemCache for table fields and table name lookups. -// -// Addressing is 3D: (group, schema, table), which eliminates schema-confusion bugs -// that existed when different schemas or database groups used the same cache key. +// Uses 3D addressing (group, schema, table) to eliminate schema-confusion bugs. // // Map value semantics: // - key absent: table not registered -// - nil value: table registered as an existence marker (fields not yet loaded) -// - non-nil: table fields have been loaded from the database +// - nil value: table registered as existence marker +// - non-nil: table fields loaded from database type tableRegistry struct { - mu sync.RWMutex - data map[tableRegistryKey]map[string]*TableField + mu sync.RWMutex + data map[tableRegistryKey]map[string]*TableField + loadedSchemas map[schemaKey]struct{} } func newTableRegistry() *tableRegistry { return &tableRegistry{ - data: make(map[tableRegistryKey]map[string]*TableField), + data: make(map[tableRegistryKey]map[string]*TableField), + loadedSchemas: make(map[schemaKey]struct{}), } } @@ -69,9 +73,7 @@ func (r *tableRegistry) SetIfNotExist(group, schema, table string) bool { return false } -// Sets marks multiple tables as known without loading their fields. -// This is more efficient than calling SetIfNotExist in a loop as it acquires the lock only once. -// If a table is already registered (with or without fields), it is skipped. +// Sets registers multiple tables and marks schema as fully loaded. func (r *tableRegistry) Sets(group, schema string, tables []string) { r.mu.Lock() defer r.mu.Unlock() @@ -81,10 +83,26 @@ func (r *tableRegistry) Sets(group, schema string, tables []string) { r.data[key] = nil } } + r.loadedSchemas[schemaKey{group, schema}] = struct{}{} +} + +// GetLoadedSchemaTables returns all registered table names for given group/schema +// and reports whether the full table list has been loaded from database. +func (r *tableRegistry) GetLoadedSchemaTables(group, schema string) (tables []string, loaded bool) { + r.mu.RLock() + defer r.mu.RUnlock() + if _, ok := r.loadedSchemas[schemaKey{group, schema}]; !ok { + return nil, false + } + for key := range r.data { + if key.group == group && key.schema == schema { + tables = append(tables, key.table) + } + } + return tables, true } -// LockFunc locks writing with given callback function `f` within RWMutex.Lock. -// This allows batch operations on the registry with a single lock acquisition. +// LockFunc executes callback function with write lock for batch operations. func (r *tableRegistry) LockFunc(f func(data map[tableRegistryKey]map[string]*TableField)) { r.mu.Lock() defer r.mu.Unlock() @@ -112,14 +130,8 @@ func (r *tableRegistry) Tables(group, schema string) []string { return tables } -// GetOrSet returns field data for the specified table, invoking loader to populate -// the registry on a cache miss. Uses double-checked locking so that: -// - concurrent reads on already-loaded tables never block each other (RLock), -// - only one goroutine executes loader per table on cold start (Lock), -// - unrelated tables' reads are not affected after the lock is released. -// -// A nil return from loader is stored as an empty map so that subsequent calls -// do not re-invoke loader (distinguishes "loaded with no fields" from "not loaded"). +// GetOrSet returns field data for specified table with cache support. +// Uses double-checked locking for concurrent safety. func (r *tableRegistry) GetOrSet( group, schema, table string, loader func() (map[string]*TableField, error), @@ -160,9 +172,10 @@ func (r *tableRegistry) Delete(group, schema, table string) { delete(r.data, tableRegistryKey{group, schema, table}) } -// ClearAll removes every entry from the registry. +// ClearAll removes all entries and resets loaded schema markers. func (r *tableRegistry) ClearAll() { r.mu.Lock() defer r.mu.Unlock() r.data = make(map[tableRegistryKey]map[string]*TableField) + r.loadedSchemas = make(map[schemaKey]struct{}) }