DBView 开发日志 ③ — SQL 日志系统、交互体验打磨与查询页面优化

DBView 开发日志 ③ --- SQL 日志系统、交互体验打磨与查询页面优化

日期: 2026-06-09 项目: DBView --- Database Visual Explorer(数据库可视化工具) 状态: v1.0 功能完善与修复阶段


一、本期概要

本期主要完成四件事:

  1. SQL 日志系统 --- 新增底部日志面板,实时记录所有数据库操作,分类展示
  2. MySQL 系统库修复 --- 修复了系统库(information_schema 等)无法加载表/索引的问题
  3. 侧边栏交互体验打磨 --- 双击连接、自动展开、断开清理等细节优化
  4. 查询页面优化 & P0 修复 --- 存储过程查看定义、分组管理、编辑器拖拽等高优问题

本期涉及 15 个文件 ,新增约 1,000 行代码 ,总源文件数达到 50 个


二、SQL 日志系统

2.1 设计思路

在开发过程中,我们逐渐发现两个痛点:

  • 调试困难: 数据库操作分散在各个驱动和 IPC handler 中,出了问题很难追溯到底执行了什么 SQL
  • 透明度不足: 用户看不到应用在后台悄悄跑了哪些查询,尤其是在自动展开树和加载元数据时

需求: 一个贯穿所有数据库操作的日志系统,实时展示每一条 SQL,按用途分类,支持筛选和查看详情。

2.2 架构:三层传递

csharp 复制代码
[DatabaseDriver]             ← 驱动层:实际执行 SQL
       ↓
[DriverLogger (装饰器)]       ← 拦截层:包装所有方法,记录日志
       ↓
[SqlLogService (主进程)]      ← 服务层:管理日志队列,推送到渲染进程
       ↓
[SqlLogPanel + logStore]     ← 渲染层:UI 展示

2.3 DriverLogger --- 装饰器模式

核心设计是 DriverLogger 装饰器 :它实现了 DatabaseDriver 接口,内部持有真实驱动实例,在每次方法调用前后插入日志逻辑:

typescript 复制代码
export class DriverLogger implements DatabaseDriver {
  constructor(
    private driver: DatabaseDriver,
    private connId: string,
    private logService: SqlLogService
  ) {}

  async executeQuery(sql: string, ...): Promise<SQLResult> {
    const start = Date.now()
    try {
      const result = await this.driver.executeQuery(sql, ...)
      this.logService.log({
        sql,
        connId,
        category: 'user',       // 用户主动执行的 SQL
        status: 'success',
        duration: Date.now() - start
      })
      return result
    } catch (err) {
      this.logService.log({
        sql,
        connId,
        category: 'user',
        status: 'error',
        duration: Date.now() - start
      })
      throw err
    }
  }

  // 元数据方法 → category: 'metadata'
  async getTables(schema?: string): Promise<TableInfo[]> {
    // 记录为 metadata 类别
  }
}

日志类别划分:

类别 场景 示例
user 用户主动执行的 SQL SQL 编辑器执行的查询、内联编辑的 INSERT/UPDATE/DELETE
system 系统自动操作 连接自动断开、连接池管理
metadata 元数据加载 getTablesgetColumnsgetDatabases

2.4 SqlLogService --- 主进程服务

主进程服务管理最多 500 条日志,通过 IPC 通道向渲染进程推送新条目:

typescript 复制代码
export class SqlLogService {
  private logs: SqlLogEntry[] = []
  private idCounter = 0

  log(params) {
    const entry = { id: this.idCounter++, timestamp: Date.now(), ...params }
    this.logs.push(entry)
    if (this.logs.length > MAX_LOG_ENTRIES) this.logs.shift()

    // 推送到所有渲染窗口
    BrowserWindow.getAllWindows().forEach((win) => {
      win.webContents.send('sql-log:entry', entry)
    })
  }
}

注意这是项目中 第一个从主进程主动推送事件到渲染进程 的模块------之前所有 IPC 都是 request/response 模式。

2.5 SqlLogPanel --- 底部面板

SqlLogPanel.tsx(297 行)是一个可折叠、可调整高度的底部面板,主要功能:

  • 折叠/展开 --- 状态栏按钮切换
  • 高度拖拽 --- 100~500px 可调
  • 分类筛选 --- All / User SQL / System / Metadata 四种
  • 状态标识 --- 绿色(成功)、红色(失败)
  • 展开详情 --- 点击日志项查看完整 SQL
  • 暗色主题适配 --- 面板背景、文字颜色全跟随主题
sql 复制代码
┌──────────────────────────────────────────────┐
│  🔍 All │ User SQL │ System │ Metadata  [清空] │  ← 筛选栏
├──────────────────────────────────────────────┤
│  ✅ SELECT * FROM users LIMIT 100       2ms  │  ← 用户 SQL
│  ❌ DROP TABLE IF EXISTS temp           15ms │
│  🔄 metadata: getDatabases(conn:xxx)    8ms  │
│  💻 system: connection pool closed      0ms  │
│  ...                                          │
├──────────────────────────────────────────────┤
│  ⇅   (拖拽手柄)                                │  ← 高度调整
└──────────────────────────────────────────────┘

2.6 logStore --- Zustand 状态

typescript 复制代码
interface LogState {
  logs: SqlLogEntry[]       // 最多 500 条
  panelVisible: boolean     // 面板折叠状态
  panelHeight: number       // 面板高度 (100-500)
  filter: 'all' | 'user' | 'system' | 'metadata'
}

2.7 驱动程序注册

ConnectionManager 中,创建驱动实例后立即用 DriverLogger 包装:

typescript 复制代码
// connection-manager.ts
const driver = DriverFactory.createDriver(config)
const loggedDriver = new DriverLogger(driver, connId, sqlLogService)

这样所有通过 ConnectionManager 获取的驱动调用都会自动记录日志,无需修改任何驱动或业务代码。


三、MySQL 系统库加载修复

3.1 问题描述

用户连接 MySQL 后,展开 information_schemamysql 等系统库时:

  • getTables() 返回空(过滤了 SYSTEM VIEW 类型)
  • getIndexes() 报错(非表对象不支持 SHOW INDEX
  • getDDL() 报错(视图不支持 SHOW CREATE TABLE

3.2 修复方案

getDatabases() --- 排序而非过滤:

之前的做法是过滤掉系统库,导致用户想看系统表时无法展开。改为排序------用户库在前、系统库在后,既不影响正常使用,也保留了对系统库的访问能力。

getTables() --- 包含 SYSTEM VIEW:

MySQL 的 information_schema 中表类型包括 SYSTEM VIEW,之前 TABLE_TYPE IN ('BASE TABLE', 'VIEW') 没有包含它。修改后支持所有类型。

getIndexes() / getDDL() --- try/catch 容错:

系统视图不支持 SHOW INDEXSHOW CREATE TABLE,加入 try/catch 后优雅降级返回空或 Create View 回退。

typescript 复制代码
// mysql-driver.ts
async getTables(schema?: string): Promise<TableInfo[]> {
  const sql = schema
    ? `SELECT TABLE_NAME, TABLE_COMMENT, ENGINE, ...
       FROM INFORMATION_SCHEMA.TABLES
       WHERE TABLE_SCHEMA = ?`
    : `SHOW TABLE STATUS WHERE 1=1`  // 当前数据库
  // 不再限制 TABLE_TYPE
}

async getIndexes(table: string, schema?: string): Promise<IndexInfo[]> {
  try {
    const [rows] = await this.query(`SHOW INDEX FROM ...`)
    // ... parse
  } catch {
    return []  // 系统视图不支持 SHOW INDEX,优雅降级
  }
}

四、侧边栏交互体验打磨

基于 Navicat 风格的侧边栏,本次进一步优化了交互细节:

4.1 双击连接节点

状态 行为
未连接 自动连接并展开
已连接 切换展开/收起
断开时 自动收起箭头并清除子节点

4.2 箭头点击自动连接

之前点连接节点旁的展开箭头时,如果未连接,树节点会卡在"加载中"状态。现在:

typescript 复制代码
// DatabaseTree.tsx --- onLoadData
const onLoadData = async (node: DataNode): Promise<void> => {
  const key = String(node.key)
  const children = await loadChildren(key)
  setTreeData((prev) => updateTreeNode(prev, key, children))
}

loadChildren 在检测到 conn 类型且未连接时,自动调用 connectionApi.connect(connId)

4.3 其他细节

  • 文字不可选中 --- user-select: none,避免双击时误选文本
  • 超长省略号 --- 连接名过长时自动截断
  • 间距调整 --- 减小箭头/图标/文字之间的间距,更紧凑
  • 背景统一白 --- 侧边栏背景颜色统一为白色,不再分层
  • 暗色主题适配 --- 树节点背景透明,跟随主题

五、P0 问题修复与查询页面优化

5.1 存储过程/函数"查看定义"

问题: 右键存储过程 → 查看定义 → 打开空白编辑器。原因是定义内容是异步加载的,但编辑器在加载完成前就显示了。

修复: 先显示"正在加载..."状态,等 getRoutineDefinition 返回后再填入编辑器内容。

5.2 连接分组管理集成

之前分组管理只能通过专门的 API 接口操作,没有 UI。现在在侧边栏右键菜单中加入了分组管理 Modal,支持:

  • 查看所有分组
  • 新建分组
  • 重命名分组
  • 删除分组(组内连接自动变为未分组)

5.3 分组内连接右键菜单

问题: 分组节点内的连接没有右键菜单。原因是 renderTitle 只在根层应用,没有递归到子节点。

修复: treeData.map 改为递归应用 renderTitle 到所有层级的节点。

5.4 连接表单弹窗布局

问题: 表单内容过长时,弹窗底部按钮被推出可见区域,需要滚动才能点击"确定"。

修复: 弹窗内容区设为 max-height: 60vh; overflow-y: auto,标题和按钮始终固定。

5.5 查询页面工具栏 & 编辑器

  • 工具栏主题跟随 --- 之前工具栏背景色硬编码为深色,在亮色主题下突兀。改为使用 Ant Design 主题变量
  • 编辑器高度可拖拽 --- SQL 编辑器底部添加拖拽手柄,高度可在 80~600px 范围内调整

六、CLAUDE.md 项目指引

新增了 CLAUDE.md(102 行)作为 Claude Code 的上下文文件,内容包括:

  • 项目概述与技术栈
  • 开发命令(pnpm dev / pnpm build
  • 进程模型(Main / Preload / Renderer)
  • IPC 通道命名规范
  • 数据库驱动架构(Strategy Pattern)
  • 状态管理(三个 Zustand Store)
  • Tab 系统说明
  • 构建配置要点
  • 已知局限

这使得后续 Claude Code 在该项目中的工作更加准确高效。


七、项目数据

指标 数值
总提交数 11
源文件数 50(.ts/.tsx)
本期新增 SQL 日志系统(4 个新文件)
本期新增/修改文件 15 个
本期新增代码 +1,003 行
本期删除代码 -67 行
最大新文件 SqlLogPanel.tsx(297 行)
最大单次提交 370b00d --- 日志模块(+685/-43,11 个文件)

八、经验与反思

做得好的

  • 装饰器模式的应用 --- DriverLogger 零侵入地给所有驱动添加了日志能力,不改任何驱动代码
  • 首个推送式 IPC --- 打破纯 request/response 模式,为后续实时功能(如查询进度通知)铺平了路
  • MySQL 降级策略 --- 用 try/catch 优雅处理系统库的不同行为,而不是粗暴过滤或一刀切

可以改进的

  • SQL 日志持久化 --- 目前日志只在内存中,会话结束后丢失。后续可考虑持久化到文件
  • 日志筛选粒度 --- 目前只有四类筛选,可以增加按连接、按表名等更细粒度的过滤
  • MySQL 系统库探索 --- 对 information_schema 的表结构展示还可以更丰富

九、下期预告

下一期将深入讲解 数据库驱动架构设计与实现,包括:

  • DatabaseDriver 接口的设计考量与演化
  • 四种数据库驱动的实现对比(MySQL / PostgreSQL / SQLite / Oracle)
  • 查询取消机制的跨驱动实现
  • 驱动测试策略

敬请期待!


DBView 开发日志系列 --- 记录一个数据库可视化工具从 0 到 1 的完整历程