️ perf(database): 优化查询元数据加载和连接释放

- 查询编辑器仅预取当前库及 SQL 显式引用库的元数据
- 断开侧边栏连接时主动释放同实例后端缓存连接
- 完善 Redis 连接释放和 Wails 前端绑定
- 修复 SQL Server 存储过程消息结果显示
- 调整查询工具栏布局并补充回归测试
Close #541
This commit is contained in:
Syngnat
2026-06-15 07:21:00 +08:00
parent 675aae16e9
commit 0b9f0448c8
13 changed files with 754 additions and 120 deletions

View File

@@ -43,6 +43,7 @@ var (
type cachedDatabase struct {
inst db.Database
lastPing time.Time
config connection.ConnectionConfig
}
type cachedConnectFailure struct {
@@ -189,6 +190,7 @@ func (a *App) Shutdown() {
logger.Error(err, "关闭数据库连接失败")
}
}
a.dbCache = make(map[string]cachedDatabase)
proxytunnel.CloseAllForwarders()
// Close all Redis connections
CloseAllRedisClients()
@@ -291,6 +293,20 @@ func getCacheKey(config connection.ConnectionConfig) string {
return hex.EncodeToString(sum[:])
}
func normalizeConnectionReleaseMatchConfig(config connection.ConnectionConfig) connection.ConnectionConfig {
normalized := normalizeCacheKeyConfig(config)
normalized.Database = ""
normalized.RedisDB = 0
return normalized
}
func getConnectionReleaseMatchKey(config connection.ConnectionConfig) string {
normalized := normalizeConnectionReleaseMatchConfig(config)
b, _ := json.Marshal(normalized)
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:])
}
func shortCacheKey(cacheKey string) string {
shortKey := cacheKey
if len(shortKey) > 12 {
@@ -726,7 +742,7 @@ func (a *App) getDatabaseWithPing(config connection.ConnectionConfig, forcePing
}
return existing.inst, nil
}
a.dbCache[key] = cachedDatabase{inst: dbInst, lastPing: now}
a.dbCache[key] = cachedDatabase{inst: dbInst, lastPing: now, config: normalizeCacheKeyConfig(effectiveConfig)}
a.mu.Unlock()
logger.Infof("数据库连接成功并写入缓存:%s 缓存Key=%s", formatConnSummary(effectiveConfig), shortKey)

View File

@@ -63,6 +63,50 @@ func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResu
return connection.QueryResult{Success: true, Message: "连接成功"}
}
func (a *App) DBReleaseConnection(config connection.ConnectionConfig) connection.QueryResult {
dbType := strings.ToLower(strings.TrimSpace(config.Type))
if dbType == "redis" {
closed, err := a.releaseRedisClientsForConfig(config)
if err != nil {
logger.Error(err, "DBReleaseConnection 释放 Redis 连接失败:%s", formatConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
logger.Infof("DBReleaseConnection 已释放 Redis 连接:%s 数量=%d", formatConnSummary(config), closed)
return connection.QueryResult{Success: true, Message: "连接已释放", Data: map[string]int{"closed": closed}}
}
resolvedConfig, err := a.resolveConnectionSecrets(config)
if err != nil {
wrapped := wrapConnectError(config, err)
logger.Error(wrapped, "DBReleaseConnection 解析连接密文失败:%s", formatConnSummary(config))
return connection.QueryResult{Success: false, Message: wrapped.Error()}
}
targetKey := getConnectionReleaseMatchKey(applyGlobalProxyToConnection(resolvedConfig))
closed := 0
a.mu.Lock()
for key, entry := range a.dbCache {
entryConfig := entry.config
if strings.TrimSpace(entryConfig.Type) == "" {
continue
}
if getConnectionReleaseMatchKey(entryConfig) != targetKey {
continue
}
if entry.inst != nil {
if closeErr := entry.inst.Close(); closeErr != nil {
logger.Error(closeErr, "DBReleaseConnection 关闭缓存连接失败缓存Key=%s", shortCacheKey(key))
}
}
delete(a.dbCache, key)
closed++
}
a.mu.Unlock()
logger.Infof("DBReleaseConnection 已释放数据库连接:%s 数量=%d", formatConnSummary(resolvedConfig), closed)
return connection.QueryResult{Success: true, Message: "连接已释放", Data: map[string]int{"closed": closed}}
}
func (a *App) TestConnection(config connection.ConnectionConfig) connection.QueryResult {
testConfig := normalizeTestConnectionConfig(config)
started := time.Now()

View File

@@ -7,6 +7,41 @@ import (
"GoNavi-Wails/internal/connection"
)
type releaseRecordingDB struct {
closed int
}
func (f *releaseRecordingDB) Connect(config connection.ConnectionConfig) error { return nil }
func (f *releaseRecordingDB) Close() error {
f.closed++
return nil
}
func (f *releaseRecordingDB) Ping() error { return nil }
func (f *releaseRecordingDB) Query(query string) ([]map[string]interface{}, []string, error) {
return nil, nil, nil
}
func (f *releaseRecordingDB) Exec(query string) (int64, error) { return 0, nil }
func (f *releaseRecordingDB) GetDatabases() ([]string, error) { return nil, nil }
func (f *releaseRecordingDB) GetTables(dbName string) ([]string, error) { return nil, nil }
func (f *releaseRecordingDB) GetCreateStatement(dbName, tableName string) (string, error) {
return "", nil
}
func (f *releaseRecordingDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
return nil, nil
}
func (f *releaseRecordingDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) {
return nil, nil
}
func (f *releaseRecordingDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
return nil, nil
}
func (f *releaseRecordingDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
return nil, nil
}
func (f *releaseRecordingDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
return nil, nil
}
func TestNormalizeTestConnectionConfig_CapsTimeout(t *testing.T) {
cfg := connection.ConnectionConfig{Timeout: 60}
got := normalizeTestConnectionConfig(cfg)
@@ -130,3 +165,44 @@ func TestFormatConnSummary_DefaultTimeout(t *testing.T) {
t.Fatalf("formatConnSummary 默认超时应为30s, got=%q", got)
}
}
func TestDBReleaseConnectionClosesAllDatabaseCacheEntriesForSameInstance(t *testing.T) {
app := NewApp()
mainConfig := connection.ConnectionConfig{Type: "mysql", Host: "127.0.0.1", Port: 3306, User: "root", Database: "main"}
analyticsConfig := mainConfig
analyticsConfig.Database = "analytics"
otherConfig := mainConfig
otherConfig.Port = 3307
otherConfig.Database = "main"
mainDB := &releaseRecordingDB{}
analyticsDB := &releaseRecordingDB{}
otherDB := &releaseRecordingDB{}
app.dbCache[getCacheKey(mainConfig)] = cachedDatabase{
inst: mainDB,
config: normalizeCacheKeyConfig(mainConfig),
}
app.dbCache[getCacheKey(analyticsConfig)] = cachedDatabase{
inst: analyticsDB,
config: normalizeCacheKeyConfig(analyticsConfig),
}
app.dbCache[getCacheKey(otherConfig)] = cachedDatabase{
inst: otherDB,
config: normalizeCacheKeyConfig(otherConfig),
}
result := app.DBReleaseConnection(connection.ConnectionConfig{Type: "mysql", Host: "127.0.0.1", Port: 3306, User: "root"})
if !result.Success {
t.Fatalf("expected release success, got %s", result.Message)
}
if mainDB.closed != 1 || analyticsDB.closed != 1 {
t.Fatalf("expected both same-instance cached connections closed, got main=%d analytics=%d", mainDB.closed, analyticsDB.closed)
}
if otherDB.closed != 0 {
t.Fatalf("expected other instance cache to remain open, got closed=%d", otherDB.closed)
}
if len(app.dbCache) != 1 {
t.Fatalf("expected only unrelated cache entry to remain, got %d", len(app.dbCache))
}
}

View File

@@ -19,6 +19,7 @@ import (
// Redis client cache
var (
redisCache = make(map[string]redis.RedisClient)
redisCacheConfigs = make(map[string]connection.ConnectionConfig)
redisCacheMu sync.Mutex
newRedisClientFunc = redis.NewRedisClient
)
@@ -60,6 +61,7 @@ func (a *App) getRedisClient(config connection.ConnectionConfig) (redis.RedisCli
}
client.Close()
delete(redisCache, key)
delete(redisCacheConfigs, key)
}
logger.Infof("创建 Redis 客户端实例缓存Key=%s", shortKey)
@@ -71,6 +73,7 @@ func (a *App) getRedisClient(config connection.ConnectionConfig) (redis.RedisCli
}
redisCache[key] = client
redisCacheConfigs[key] = normalizeCacheKeyConfig(connectedConfig)
logger.Infof("Redis 连接成功并写入缓存:%s 缓存Key=%s", formatRedisConnSummary(connectedConfig), shortKey)
return client, nil
}
@@ -142,6 +145,35 @@ func getRedisClientCacheKey(config connection.ConnectionConfig) string {
return hex.EncodeToString(sum[:])
}
func (a *App) releaseRedisClientsForConfig(config connection.ConnectionConfig) (int, error) {
resolvedConfig, err := a.resolveConnectionSecrets(config)
if err != nil {
return 0, wrapConnectError(config, err)
}
targetKey := getConnectionReleaseMatchKey(applyGlobalProxyToConnection(resolvedConfig))
closed := 0
redisCacheMu.Lock()
defer redisCacheMu.Unlock()
for key, client := range redisCache {
entryConfig := redisCacheConfigs[key]
if strings.TrimSpace(entryConfig.Type) == "" {
continue
}
if getConnectionReleaseMatchKey(entryConfig) != targetKey {
continue
}
if client != nil {
client.Close()
}
delete(redisCache, key)
delete(redisCacheConfigs, key)
closed++
}
return closed, nil
}
func formatRedisConnSummary(config connection.ConnectionConfig) string {
var b strings.Builder
b.WriteString("类型=redis 地址=")
@@ -759,4 +791,5 @@ func CloseAllRedisClients() {
}
}
redisCache = make(map[string]redis.RedisClient)
redisCacheConfigs = make(map[string]connection.ConnectionConfig)
}