在数据分析平台越来越卷的今天,各家都在琢磨怎么让用户更直观地理解自己的数据。
笔者所在团队维护着一个数据分析平台(技术栈:React18 + Vite + TypeScript + Ant Design),产品同学提了一个需求:用户导入数据库后,希望能以可视化的方式展示表结构及表之间的关系,就像数据库设计工具里的 ER 图那样。
听起来不难是吧?然而后端同学给的数据是——数据库的 DDL 语句。
好吧,SQL 解析这活儿看来得前端自己想办法了。
需求分析
先捋一下需求:
- 输入:用户导入的数据库 DDL 语句(
CREATE TABLE语句) - 输出:可视化的 ER 图,展示表结构、字段信息、表之间的关联关系
- 交互:支持拖拽、缩放、节点展开/收起等常见操作
核心问题有两个:
- SQL 解析:如何把 DDL 语句解析成结构化的表数据?
- 图渲染:如何把表数据渲染成好看的 ER 图?
技术选型
SQL 解析库
在 GitHub 上一顿搜索,找到了几个候选方案:
| 库名 | Stars | 特点 |
|---|---|---|
| sql.js | 13k+ | SQLite 的 WebAssembly 版本,偏重执行而非解析 |
| pgsql-ast-parser | 200+ | 只支持 PostgreSQL |
| node-sql-parser | 1k+ | 支持多种数据库,解析成标准 AST |
最终选择了 node-sql-parser,原因很简单:
- 支持 11 种数据库方言(MySQL、PostgreSQL、SQLite、MariaDB、SQL Server 等)
- 解析结果是标准的 AST,方便提取表结构和外键信息
- 文档清晰,API 简洁
import { Parser } from "node-sql-parser";
const parser = new Parser();
const ast = parser.astify(`
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
)
`, { database: "MySQL" });
console.log(ast);
// 输出完整的 AST 结构
可视化组件
图可视化这块,首先想到的是 D3.js,但 D3 太底层了,画个节点连线都得从头写,不太划算。
继续调研,发现了 React Flow,这个库专门为 React 设计,API 友好,自带很多交互能力:
- 节点拖拽
- 画布缩放
- 小地图导航
- 连线动画
- 自定义节点样式
配合 Dagre 布局算法,可以实现节点的自动排列,不用手动调整位置。
import { ReactFlow, Background, MiniMap, Controls } from "@xyflow/react";
function ERDiagram({ nodes, edges }) {
return (
<ReactFlow nodes={nodes} edges={edges}>
<Background />
<MiniMap />
<Controls />
</ReactFlow>
);
}
整体设计
确定了技术选型,接下来设计整体架构。
数据流
用户输入 SQL DDL
↓
node-sql-parser 解析
↓
TableData[] + RelationshipData
↓
转换为 React Flow 节点和边
↓
Dagre 自动布局
↓
React Flow 渲染 ER 图
项目结构
sql-to-er-table/
├── client/
│ ├── pages/SqlToER/
│ │ └── SqlToERPage.tsx # 主页面
│ ├── components/ERDiagram/
│ │ ├── ERDiagram.tsx # 图容器
│ │ ├── ERNode.tsx # 自定义表节点
│ │ ├── ERDiagramParser.ts # 数据转换
│ │ └── utils.ts # 布局算法
│ └── utils/
│ └── sqlParser.ts # SQL 解析(API 调用)
│
├── server/
│ ├── services/
│ │ └── sqlParser.ts # SQL 解析服务
│ └── middleware/
│ └── serveApi.ts # API 路由
│
└── shared/
├── types.ts # 共享类型
└── crypto.ts # 加密工具
类型定义
定义好数据结构,前后端共享:
// shared/types.ts
export interface ColumnSchema {
type: string;
nullable: boolean;
comment: string;
}
export interface TableData {
table_name: string;
name: string;
comment: string | null;
schema: Record<string, ColumnSchema>;
index_info?: {
primary_key?: string[];
};
}
export interface RelationshipData {
relationships: string[][]; // [["orders.user_id", "users.id"], ...]
}
优化点一:SQL 解析放服务端
一开始图省事,SQL 解析直接在浏览器端做。跑起来之后发现一个问题:node-sql-parser 打包后有 410KB+,直接把 client bundle 撑大了一圈。
对于一个工具页面来说,这个体积有点夸张。而且 SQL 解析本身是纯计算任务,放在服务端更合理。
改造思路
- 服务端新增
/api/parse-sql接口,接收 SQL 语句,返回解析结果 - 客户端改为调用 API,不再直接依赖
node-sql-parser - 前端 bundle 瞬间瘦身
服务端实现:
// server/services/sqlParser.ts
import nodeSqlParser from "node-sql-parser";
import type { TableData, RelationshipData, DatabaseType } from "../../shared/types";
const { Parser } = nodeSqlParser;
const sqlParser = new Parser();
export function parseSqlToERData(sql: string, database: DatabaseType = "MySQL") {
const errors: string[] = [];
const tables: TableData[] = [];
try {
const result = sqlParser.astify(sql, { database });
const astList = Array.isArray(result) ? result : [result];
for (const ast of astList) {
if (ast?.type !== "create" || ast?.keyword !== "table") continue;
const tableData = parseCreateTableAST(ast);
tables.push(tableData);
}
} catch (err: any) {
errors.push(`SQL 解析失败:${err?.message}`);
}
return { tables, relationships, errors };
}
客户端调用:
// client/utils/sqlParser.ts
export async function parseSqlToERData(sql: string, database: DatabaseType = "MySQL") {
const response = await fetch("/api/parse-sql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sql, database }),
});
return response.json();
}
改造完成后,client bundle 下降了 400KB,效果显著。
- 改造前:
- 改造后:
优化点二:SQL 加密传输
需求评审的时候,安全同学提了一个问题:SQL 语句里可能包含敏感信息(表名、字段名、注释等),明文传输不太合适。
好吧,那就加个密。
加密方案
考虑到是内部系统,不需要非常复杂的加密体系,选择了 AES-256-GCM 对称加密:
- 加密强度足够
- 自带认证标签(AuthTag),可以防止数据被篡改
- 前后端都有成熟的实现
客户端使用 Web Crypto API:
// client/utils/crypto.ts
const ENCRYPTION_KEY = "sql-er-diagram-secret-key-32byte!";
async function getEncryptionKey(): Promise<CryptoKey> {
const keyData = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(ENCRYPTION_KEY)
);
return crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
);
}
export async function encryptSql(sql: string): Promise<string> {
const key = await getEncryptionKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode(sql)
);
// 组合格式: iv:authTag:ciphertext (均为 base64)
const encryptedArray = new Uint8Array(encrypted);
const ciphertext = encryptedArray.slice(0, -16);
const authTag = encryptedArray.slice(-16);
return `${btoa(iv)}:${btoa(authTag)}:${btoa(ciphertext)}`;
}
服务端使用 Node.js crypto 模块解密:
// shared/crypto.ts
import crypto from "crypto";
export function decryptSql(payload: string): string {
const [ivBase64, authTagBase64, encrypted] = payload.split(":");
const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
const iv = Buffer.from(ivBase64, "base64");
const authTag = Buffer.from(authTagBase64, "base64");
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, "base64", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
现在 SQL 传输流程变成了:
客户端输入 SQL
↓
AES-256-GCM 加密
↓
POST /api/parse-sql { payload: "加密后的字符串" }
↓
服务端解密
↓
node-sql-parser 解析
↓
返回解析结果
最终效果
经过一番折腾,终于实现了从 SQL DDL 到 ER 图的完整流程:
- 用户在输入框粘贴 SQL 语句
- 选择数据库类型(支持 MySQL、PostgreSQL 等 11 种)
- 点击「生成 ER 图」
- 自动渲染出带关系连线的 ER 图
- 支持拖拽、缩放、小地图导航
总结
技术选型优点
- node-sql-parser:支持多种数据库方言,解析结果标准化,满足大部分 DDL 解析需求
- React Flow:专为 React 设计的图可视化库,开箱即用,交互体验好
- Dagre:经典的图布局算法,自动排列节点位置,省去手动调整的麻烦
- AES-256-GCM:加密强度足够,自带完整性校验,前后端都有成熟实现
不足之处
- SQL 解析的局限性:
node-sql-parser对一些复杂语法(如存储过程、触发器)支持有限,部分非标准写法可能解析失败 - 关系识别依赖外键:目前只能通过
FOREIGN KEY约束识别表关系,实际业务中很多表并没有显式定义外键 - 布局算法的局限:Dagre 是基于层次结构的布局,对于复杂的网状关系,布局效果可能不太理想
- 客户端加密的安全性:密钥硬编码在前端代码中,安全性有限,仅适用于内部系统
后续优化方向
- 支持通过字段命名规则(如
user_id->users.id)智能识别表关系 - 支持导出 ER 图为图片或 PDF
- 考虑使用 WebAssembly 方案,在保证性能的同时减少服务端依赖
项目代码已开源(脱敏处理),欢迎查看 👉 sql-to-er-table