DBView 开发日志 ③ --- SQL 日志系统、交互体验打磨与查询页面优化
日期: 2026-06-09 项目: DBView --- Database Visual Explorer(数据库可视化工具) 状态: v1.0 功能完善与修复阶段
一、本期概要
本期主要完成四件事:
- SQL 日志系统 --- 新增底部日志面板,实时记录所有数据库操作,分类展示
- MySQL 系统库修复 --- 修复了系统库(
information_schema等)无法加载表/索引的问题 - 侧边栏交互体验打磨 --- 双击连接、自动展开、断开清理等细节优化
- 查询页面优化 & 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 |
元数据加载 | getTables、getColumns、getDatabases 等 |
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_schema 或 mysql 等系统库时:
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 INDEX 和 SHOW 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 的完整历程