feat(schema): 增加数据库对象列表缓存

This commit is contained in:
t8y2
2026-05-09 17:36:20 +08:00
parent 1fd1cb10eb
commit 5c52560368
12 changed files with 261 additions and 50 deletions
+8 -8
View File
@@ -451,9 +451,9 @@ async function confirmDropTable() {
await api.executeQuery(node.connectionId, node.database, buildDropTableSql(), node.schema);
toast(t("contextMenu.dropTableSuccess", { name: node.label }), 3000);
if (node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
await connectionStore.loadTables(node.connectionId, node.database, node.schema, { force: true });
} else {
await connectionStore.loadTables(node.connectionId, node.database);
await connectionStore.loadTables(node.connectionId, node.database, undefined, { force: true });
}
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
@@ -518,7 +518,7 @@ async function confirmCreateDatabase() {
const sql = `CREATE DATABASE ${quoteIdent(name)};`;
await api.executeQuery(node.connectionId, "", sql);
toast(t("contextMenu.createDatabaseSuccess", { name }), 3000);
await connectionStore.loadDatabases(node.connectionId);
await connectionStore.loadDatabases(node.connectionId, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -535,7 +535,7 @@ async function confirmDropDatabase() {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, "", buildDropDatabaseSql());
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
await connectionStore.loadDatabases(node.connectionId);
await connectionStore.loadDatabases(node.connectionId, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -556,7 +556,7 @@ async function confirmCreateSchema() {
const sql = `CREATE SCHEMA ${quoteIdent(name)};`;
await api.executeQuery(node.connectionId, node.database, sql);
toast(t("contextMenu.createSchemaSuccess", { name }), 3000);
await connectionStore.loadSchemas(node.connectionId, node.database);
await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -573,7 +573,7 @@ async function confirmDropSchema() {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildDropSchemaSql());
toast(t("contextMenu.dropSchemaSuccess", { name: node.label }), 3000);
await connectionStore.loadSchemas(node.connectionId, node.database);
await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -609,9 +609,9 @@ async function confirmDuplicateStructure() {
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }), 3000);
if (node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
await connectionStore.loadTables(node.connectionId, node.database, node.schema, { force: true });
} else {
await connectionStore.loadTables(node.connectionId, node.database);
await connectionStore.loadTables(node.connectionId, node.database, undefined, { force: true });
}
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
+3
View File
@@ -39,6 +39,9 @@ export const loadConnections = forward("loadConnections");
// Schema
export const listDatabases = forward("listDatabases");
export const saveSchemaCache = forward("saveSchemaCache");
export const loadSchemaCache = forward("loadSchemaCache");
export const deleteSchemaCachePrefix = forward("deleteSchemaCachePrefix");
export const listSchemas = forward("listSchemas");
export const listTables = forward("listTables");
export const getColumns = forward("getColumns");
+12
View File
@@ -96,6 +96,18 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
return get(`/api/schema/databases?${qs({ connection_id: connectionId })}`);
}
export async function saveSchemaCache(cacheKey: string, payload: unknown): Promise<void> {
return post("/api/schema/cache", { cacheKey, payload });
}
export async function loadSchemaCache<T = unknown>(cacheKey: string): Promise<T | null> {
return get(`/api/schema/cache?${qs({ cache_key: cacheKey })}`);
}
export async function deleteSchemaCachePrefix(prefix: string): Promise<void> {
return del(`/api/schema/cache-prefix?${qs({ prefix })}`);
}
export async function listSchemas(connectionId: string, database: string): Promise<string[]> {
return get(`/api/schema/schemas?${qs({ connection_id: connectionId, database })}`);
}
+12
View File
@@ -117,6 +117,18 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
return invoke("list_databases", { connectionId });
}
export async function saveSchemaCache(cacheKey: string, payload: unknown): Promise<void> {
return invoke("save_schema_cache", { cacheKey, payload });
}
export async function loadSchemaCache<T = unknown>(cacheKey: string): Promise<T | null> {
return invoke("load_schema_cache", { cacheKey });
}
export async function deleteSchemaCachePrefix(prefix: string): Promise<void> {
return invoke("delete_schema_cache_prefix", { prefix });
}
export async function listTables(connectionId: string, database: string, schema: string): Promise<TableInfo[]> {
return invoke("list_tables", { connectionId, database, schema });
}
+93 -42
View File
@@ -25,6 +25,10 @@ import { buildDatabaseTreeNodes } from "@/lib/databaseTree";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
interface LoadTreeOptions {
force?: boolean;
}
export const useConnectionStore = defineStore("connection", () => {
const connections = ref<ConnectionConfig[]>([]);
const isDesktop = isTauriRuntime();
@@ -38,6 +42,7 @@ export const useConnectionStore = defineStore("connection", () => {
const treeNodes = ref<TreeNode[]>([]);
const pinnedTreeNodeIds = ref<Set<string>>(loadPinnedTreeNodeIds());
const connectedIds = ref<Set<string>>(new Set());
const loadedTreeNodeChildrenIds = ref<Set<string>>(new Set());
const connectionErrors = ref<Record<string, string>>({});
const editingConnectionId = ref<string | null>(null);
const newConnectionGroupId = ref<string | null>(null);
@@ -188,6 +193,38 @@ export const useConnectionStore = defineStore("connection", () => {
function setChildren(parent: TreeNode, children: TreeNode[]) {
parent.children = orderPinnedFirst(children.map(pinTreeNode), (node) => !!node.pinned);
loadedTreeNodeChildrenIds.value.add(parent.id);
}
function schemaCacheKey(...parts: string[]): string {
return parts.map((part) => encodeURIComponent(part)).join(":");
}
async function loadPersistedTreeChildren(node: TreeNode, cacheKey: string): Promise<boolean> {
const children = await api.loadSchemaCache<TreeNode[]>(cacheKey).catch(() => null);
if (!children) return false;
setChildren(node, children);
node.isExpanded = true;
return true;
}
async function savePersistedTreeChildren(cacheKey: string, children: TreeNode[]) {
await api.saveSchemaCache(cacheKey, children).catch(() => undefined);
}
function useCachedChildren(node: TreeNode, options?: LoadTreeOptions): boolean {
if (options?.force || !loadedTreeNodeChildrenIds.value.has(node.id)) return false;
node.isExpanded = true;
return true;
}
function clearLoadedChildrenCache(prefix: string) {
for (const id of loadedTreeNodeChildrenIds.value) {
if (id === prefix || id.startsWith(`${prefix}:`)) {
loadedTreeNodeChildrenIds.value.delete(id);
}
}
api.deleteSchemaCachePrefix(`${schemaCacheKey(prefix)}:`).catch(() => undefined);
}
function findParentNode(nodes: TreeNode[], id: string, parent: TreeNode | null = null): TreeNode | null {
@@ -263,6 +300,7 @@ export const useConnectionStore = defineStore("connection", () => {
activeConnectionId.value = null;
}
invalidateCompletionCache(id);
clearLoadedChildrenCache(id);
}
async function updateConnection(config: ConnectionConfig) {
@@ -276,6 +314,7 @@ export const useConnectionStore = defineStore("connection", () => {
rebuildTreeNodes();
connectedIds.value.delete(config.id);
invalidateCompletionCache(config.id);
clearLoadedChildrenCache(config.id);
}
async function setDefaultDatabase(connectionId: string, database: string) {
@@ -343,6 +382,7 @@ export const useConnectionStore = defineStore("connection", () => {
node.isExpanded = false;
node.children = [];
}
clearLoadedChildrenCache(connectionId);
if (activeConnectionId.value === connectionId) {
activeConnectionId.value = null;
}
@@ -368,15 +408,20 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadDatabases(connectionId: string) {
async function loadDatabases(connectionId: string, options?: LoadTreeOptions) {
const node = findNode(treeNodes.value, connectionId);
if (!node) return;
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, "databases");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const databases = await api.listDatabases(connectionId);
setChildren(node, buildDatabaseTreeNodes(connectionId, databases));
const children = buildDatabaseTreeNodes(connectionId, databases);
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
} finally {
node.isLoading = false;
@@ -460,57 +505,63 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadSchemas(connectionId: string, database: string) {
async function loadSchemas(connectionId: string, database: string, options?: LoadTreeOptions) {
const nodeId = `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
if (!node) return;
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, database, "schemas");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const schemas = await api.listSchemas(connectionId, database);
setChildren(
node,
schemas.map((s) => ({
id: `${connectionId}:${database}:${s}`,
label: s,
type: "schema" as const,
connectionId,
database,
schema: s,
isExpanded: false,
children: [],
})),
);
const children = schemas.map((s) => ({
id: `${connectionId}:${database}:${s}`,
label: s,
type: "schema" as const,
connectionId,
database,
schema: s,
isExpanded: false,
children: [],
}));
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
} finally {
node.isLoading = false;
}
}
async function loadTables(connectionId: string, database: string, schema?: string) {
async function loadTables(connectionId: string, database: string, schema?: string, options?: LoadTreeOptions) {
const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
if (!node) return;
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "tables");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const querySchema = schema || database;
const tables = await api.listTables(connectionId, database, querySchema);
const config = getConfig(connectionId);
const effectiveSchema = schema || (config?.db_type && isSchemaAware(config.db_type) ? database : undefined);
setChildren(
node,
tables.map((t) => ({
id: `${nodeId}:${t.name}`,
label: t.name,
type: (t.table_type === "VIEW" ? "view" : "table") as "view" | "table",
connectionId,
database,
schema: effectiveSchema,
isExpanded: false,
children: [],
})),
);
const children = tables.map((t) => ({
id: `${nodeId}:${t.name}`,
label: t.name,
type: (t.table_type === "VIEW" ? "view" : "table") as "view" | "table",
connectionId,
database,
schema: effectiveSchema,
isExpanded: false,
children: [],
}));
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
} finally {
node.isLoading = false;
@@ -696,7 +747,7 @@ export const useConnectionStore = defineStore("connection", () => {
return ids;
}
async function loadTreeNodeChildren(node: TreeNode) {
async function loadTreeNodeChildren(node: TreeNode, options?: LoadTreeOptions) {
if (node.type === "connection" && node.connectionId) {
const config = getConfig(node.connectionId);
if (config?.db_type === "redis") {
@@ -704,19 +755,19 @@ export const useConnectionStore = defineStore("connection", () => {
} else if (config?.db_type === "mongodb" || config?.db_type === "elasticsearch") {
await loadMongoDatabases(node.connectionId);
} else {
await loadDatabases(node.connectionId);
await loadDatabases(node.connectionId, options);
}
} else if (node.type === "mongo-db" && node.connectionId && node.database) {
await loadMongoCollections(node.connectionId, node.database);
} else if (node.type === "database" && node.connectionId && node.database) {
const config = getConfig(node.connectionId);
if (config?.db_type && TREE_SCHEMA_TYPES.has(config.db_type)) {
await loadSchemas(node.connectionId, node.database);
await loadSchemas(node.connectionId, node.database, options);
} else {
await loadTables(node.connectionId, node.database);
await loadTables(node.connectionId, node.database, undefined, options);
}
} else if (node.type === "schema" && node.connectionId && node.database && node.schema) {
await loadTables(node.connectionId, node.database, node.schema);
await loadTables(node.connectionId, node.database, node.schema, options);
} else if ((node.type === "table" || node.type === "view") && node.connectionId && node.database) {
await loadTableGroups(node.connectionId, node.database, node.label, node.schema);
} else if (node.type === "group-columns" && node.connectionId && node.database && node.tableName) {
@@ -730,12 +781,12 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function restoreExpandedChildren(node: TreeNode, expandedIds: Set<string>) {
async function restoreExpandedChildren(node: TreeNode, expandedIds: Set<string>, options?: LoadTreeOptions) {
if (!node.children) return;
for (const child of node.children) {
if (!expandedIds.has(child.id)) continue;
await loadTreeNodeChildren(child);
await restoreExpandedChildren(child, expandedIds);
await loadTreeNodeChildren(child, options);
await restoreExpandedChildren(child, expandedIds, options);
}
}
@@ -745,8 +796,8 @@ export const useConnectionStore = defineStore("connection", () => {
if (node.type !== "connection-group") {
node.children = [];
}
await loadTreeNodeChildren(node);
await restoreExpandedChildren(node, expandedIds);
await loadTreeNodeChildren(node, { force: true });
await restoreExpandedChildren(node, expandedIds, { force: true });
}
function isSchemaAwareDatabase(connectionId: string): boolean {
@@ -874,8 +925,8 @@ export const useConnectionStore = defineStore("connection", () => {
}
if (!expandedIds.has(node.id)) continue;
node.children = [];
await loadTreeNodeChildren(node);
await restoreExpandedChildren(node, expandedIds);
await loadTreeNodeChildren(node, { force: true });
await restoreExpandedChildren(node, expandedIds, { force: true });
}
};
await refreshExpandedNodes(treeNodes.value);