上个月我接手一个内部管理系统,代码还没看,先栽在数据库上。项目里有50多张表,文档目录只有一个三年前的Word,里面只写了十来张核心表,字段说明还停留在“备注”“状态”“创建时间”这种级别。改一个老功能,我需要来回扫SQL、看实体类、翻初始化脚本才能确定某张表到底是干什么的。折腾到第三天我实在受不了,决定把表结构文档这件事一次性解决。
我当时想的是:能不能用一个现成工具,连接一次数据库,自动把所有表的字段、类型、注释、索引、建表语句全部导出来,最好是Markdown或HTML,能直接扔进项目文档库。后来找到的就是screw-core,一个Java生态里的数据库表结构文档生成工具。这篇文章就记录我怎么用它把一张空配置跑通、怎么把文档做到能直接交付、以及过程中踩到的坑。
1. 为什么表结构文档这种事,值得用自动化工具去解决
很多团队其实不是不想写文档,而是“写文档”这个动作在数据库场景里天然反人性。表结构每天都在变,今天加一个字段,明天删一个索引,手写文档根本跟不上。更麻烦的是,建表时的注释质量参差不齐,有些表甚至没有COMMENT,你对着这种库去写文档,光猜表含义就能耗掉半天。
1.1 表结构文档的真正价值是“让接手的人少踩坑”
数据库表结构文档不是给DBA看的,是给三类人看的:新接手项目的开发、写报表和数据分析的同事、以及做代码评审的你自己。我见过太多项目,代码里Service层写得清清楚楚,但底层表的关系全靠口口相传。一旦负责核心模块的人离职,那些“这个表是中间表,逻辑已废弃但数据不能删”之类的隐性知识就全丢了。
screw-core这类工具解决的是“文档从无到有”的问题。它能把数据库里已有的元数据信息,包括表名、表注释、字段名、字段注释、字段类型、是否主键、是否为空、默认值、索引信息,统一抽出来,渲染成结构化的文档。这比任何手工维护都可靠,因为它是直接从数据库读的,只要表结构是真的,文档就不会说谎。
1.2 自动化生成文档的前提是:你的库本身有注释规范
这里必须先说一句不太好听的话:工具只能放大你数据库里的信息质量,不能凭空创造。如果建表语句里没有COMMENT,生成的文档里“列注释”那一栏就是空的。所以我在跑通screw-core之后,做的第一件事不是去看文档效果,而是去补齐几张核心表的COMMENT。
比如:
ALTER TABLE `order` MODIFY COLUMN `order_status` TINYINT(4) COMMENT '订单状态:0-待支付 1-已支付 2-已取消';这件事最好在平时开发时养成习惯。没有表注释和字段注释的库,用什么工具生成文档都是半成品。screw-core能给你一个非常清晰的反面清单——它输出的Markdown里,注释为空的地方一目了然,你照着去补就行。
2. screw-core生成文档的原理,以及它和手工导出的本质区别
我第一次看到这个工具时以为它是类似mysqldump那种导出工具,后来看了源码才明白,它走的是JDBC的DatabaseMetaData接口。什么意思?它不解析物理文件,也不执行复杂的查询,而是通过数据库驱动暴露的元数据接口,拿到库里的表结构信息,再套用模板渲染成文档。
2.1 简单说就是:连接数据库、读元数据、套模板、出文件
整个过程可以拆成四步:
- 你提供一个
DataSource,它负责和数据库建立连接。 - screw-core调用JDBC的元数据接口,拿到所有表的字段、主键、索引、外键等信息。
- 数据被填充到FreeMarker或Velocity模板里。
- 引擎根据你指定的文件类型,输出Markdown、HTML或者Word。
这个设计的好处是:它不依赖你用的是MySQL还是PostgreSQL,只要数据库驱动实现了JDBC标准接口,理论上都能接。实际使用中我主要拿它连MySQL,后来也在PostgreSQL上试过,核心逻辑没有改。
2.2 和Navicat导出、PlantUML、手写文档放在一起比
我把常见方案对比了一圈:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Navicat“导出数据库” | 操作快,能拿到SQL和部分结构 | 生成的是SQL脚本,不是给人看的文档,而且没有漂亮的排版 |
| PlantUML画ER图 | 图形化展示表关系 | 表多之后完全没法维护,一张张画到猴年马月 |
| 手写Word/Markdown | 可以自由裁剪,写业务说明 | 维护成本极高,表结构一变就废 |
| screw-core自动生成 | 一键生成、格式统一、带索引和DDL | 依赖注释质量,不包含业务逻辑说明 |
我最后选screw-core,核心原因是它输出稳定、可重复执行。数据库表结构变了,重新跑一次就行,文档永远是“最新状态”。这对手头有几十张表的项目来说,省的时间是实打实的。
3. 最简单可跑通的例子:一个Java类生成Markdown文档
我一开始没有搞Maven插件,也没有接流水线,就是写了一个最普通的Java主类,连上测试库,跑通之后才逐步加配置。下面这个是缩小到不能再小的版本。
3.1 新建工程并引入三件套依赖
我用的是Spring Boot项目,但这里不需要启动Spring,一个普通的Maven工程足够。关键依赖就三个:
<!-- screw-core 文档生成核心 --> <dependency> <groupId>cn.smallbun.screw</groupId> <artifactId>screw-core</artifactId> <version>1.0.5</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency>需要注意,screw-core本身会依赖Freemarker或Velocity这类模板引擎,但不同小版本的传递依赖情况不太一样。保险起见,如果你在运行时遇到ClassNotFoundException: freemarker...之类的错误,手动补上对应模板引擎依赖就行。
3.2 核心代码:一个main方法,三步生成
下面这个例子是我在1.0.5版本上实测跑的,只生成Markdown,不打开输出目录:
package com.example.docgen; import cn.smallbun.screw.core.Screw; import cn.smallbun.screw.core.engine.EngineConfig; import cn.smallbun.screw.core.engine.EngineFileType; import cn.smallbun.screw.core.engine.EngineTemplateType; import cn.smallbun.screw.core.process.ProcessConfig; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.util.ArrayList; import java.util.List; public class TableDocGenerator { public static void main(String[] args) { // 1. 配置数据源 HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/demo_db" + "?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"); hikariConfig.setUsername("root"); hikariConfig.setPassword("your_password"); // 这个属性很关键,关系到能否读到表注释 hikariConfig.addDataSourceProperty("useInformationSchema", "true"); hikariConfig.setMinimumIdle(2); hikariConfig.setMaximumPoolSize(5); DataSource dataSource = new HikariDataSource(hikariConfig); // 2. 配置文档引擎 EngineConfig engineConfig = EngineConfig.builder() .fileOutputDir("/Users/me/db-doc") .openOutputDir(false) .fileType(EngineFileType.MD) .produceType(EngineTemplateType.freemarker) .build(); // 3. 配置处理范围,这里先不忽略任何表 ProcessConfig processConfig = ProcessConfig.builder() .build(); // 4. 执行生成 new Screw().generate(dataSource, engineConfig, processConfig); System.out.println("数据库表结构文档生成完成"); } }这里有一个地方我想单独提一下:HikariConfig里的addDataSourceProperty("useInformationSchema", "true")。如果不加这个,MySQL驱动默认可能拿不到TABLE_COMMENT,生成的文档里表注释会变成空。这是我在第一次跑的时候就遇到的坑,后面会详细说。
3.3 输出结果是什么样
默认生成的Markdown文件名字类似数据库表结构设计文档_20241020_153000.md,打开之后结构非常整齐:
## 1. 表结构 ### 1.1 demo_user **表名**:demo_user **表注释**:用户基础信息表 **表引擎**:InnoDB **表字符集**:utf8mb4 #### 字段列表 | 序号 | 列名 | 数据类型 | 长度 | 小数位 | 允许空 | 默认值 | 主键 | 列注释 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | id | bigint | 20 | 0 | NO | NULL | YES | 主键ID | | 2 | username | varchar | 64 | 0 | NO | NULL | NO | 用户名 | | 3 | created_at | datetime | 0 | 0 | YES | NULL | NO | 创建时间 |如果你用的是标准建表语句,每个字段都有注释,这份Markdown基本可以直接作为交付文档提交到Git仓库。跟手写的相比,至少不会出现“字段名写错”“类型对不上”“漏了索引”这类问题。
3.4 为什么我建议先从代码方式跑,而不是直接用Maven插件
网上很多教程会直接让你配screw-maven-plugin,配好之后一行命令就能跑。但我在给团队内部培训时的经验是:第一次上手,最好先用Java代码跑通。
原因是代码方式暴露了所有的配置项,你能清楚地看到数据源是怎么连接的、引擎配置有哪些、处理范围是怎么控制的。一旦出了问题,你也更容易定位是连接问题还是配置问题。插件方式虽然命令短,但相当于把细节藏起来了,出了错反而难排查。
4. 让文档能直接交付的关键配置项
跑通最简例子之后,你会发现要真正把文档用在项目里,还得控制“生成哪些表”“输出到哪里”“标题版本是什么”。这一节我讲几个我认为最有价值的配置。
4.1 ProcessConfig:只生成需要的表,把垃圾表排除掉
真实业务库里通常会有很多不想出现在文档里的表,比如sys_config、quartz相关表、临时表、日志表。全量生成会导致文档又长又没重点。ProcessConfig提供了三种过滤方式:
List<String> ignoreTableName = new ArrayList<>(); ignoreTableName.add("sys_config"); List<String> ignoreTablePrefix = new ArrayList<>(); ignoreTablePrefix.add("tmp_"); List<String> ignoreTableSuffix = new ArrayList<>(); ignoreTableSuffix.add("_log"); ProcessConfig processConfig = ProcessConfig.builder() .ignoreTableName(ignoreTableName) .ignoreTablePrefix(ignoreTablePrefix) .ignoreTableSuffix(ignoreTableSuffix) .build();这几个配置的含义很清楚:按表名精确忽略、按表名前缀忽略、按表名后缀忽略。我实际生产环境里用的最多的是ignoreTablePrefix和ignoreTableSuffix,因为很多工程的临时表和日志表命名规律比较统一。反向的还有designatedTableName,指定只生成某几张表,适合那种只想给核心模块出文档的场景。
4.2 EngineConfig:版本号、输出目录、文件类型一次性设好
EngineConfig决定了文档最终长成什么样。我最常用的配置项:
EngineConfig engineConfig = EngineConfig.builder() .fileOutputDir("/data/docs/database") // 输出目录 .openOutputDir(true) // 生成后自动打开目录 .fileType(EngineFileType.HTML) // 支持MD、HTML、WORD .produceType(EngineTemplateType.freemarker) .fileName("用户中心数据库设计文档") // 自定义文件名 .build();需要注意,EngineFileType枚举里的WORD生成的是.doc文件,本质是Word可以打开的HTML格式,并不是严格的.docx。如果你的交付要求是docx,可能需要借助Word另存一下,或者接受这个格式。我一直用MD和HTML两个格式,MD放Git仓库方便Diff,HTML发给业务方看着美观。
4.3 文档里的“版本”信息,我建议留一手
screw-core生成文档时会带上版本号和描述信息,格式大概像“数据库设计文档 v1.0”。默认值是死的,但你可以通过引擎配置或者数据源信息去影响它。我一般会把版本号设置成项目的版本号,比如v2.3.0,这样以后翻旧文档时能快速判断这份文档对应哪个迭代。
另外还有一个比较细节的点:输出文件名里默认带时间戳,好处是不会覆盖历史文档。但如果你希望每次生成的文档都是同一个文件名,方便被其他系统引用,可以设置固定的fileName。我的做法是:本地开发用固定文件名,发版时归档到带日期的目录。
5. 真实踩坑记录:时区、编码、元数据开关
这一节我想分享的不是“怎么配”,而是“出了问题怎么排查”。因为网上关于screw-core的教程很多,但真正让你卡住半小时的往往是这些小问题。
5.1 serverTimezone导致的连接报错和乱码
我第一次跑的时候,JDBC连接串只写了jdbc:mysql://127.0.0.1:3306/demo_db,结果启动直接报时区错误。MySQL 8.x对时区要求比较严谨,必须在连接串里指定serverTimezone。
hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/demo_db" + "?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");这里的characterEncoding=UTF-8也很关键。如果漏掉,字段注释里有中文时,生成的文档可能会出现??之类的乱码。虽然现在的MySQL驱动默认会做UTF-8编码,但显式写出来最稳妥。
5.2 useInformationSchema这个参数,直接影响表注释
我第一次生成的Markdown里所有表名都对,字段也对,但“表注释”一栏全是空的。我当时第一反应是数据库里的表本来就没写注释,结果去Navicat里一看,每张表都有COMMENT。
查了一圈才知道,MySQL驱动读取表注释的机制和information_schema有关。默认情况下,Connector/J为了性能考虑,可能不主动去查information_schema里的表注释,需要你在连接池配置里强制打开:
hikariConfig.addDataSourceProperty("useInformationSchema", "true");加上这一行之后重新生成,表注释就出来了。这个坑很隐蔽,因为它不影响查数据,只影响元数据读取。
5.3 模板引擎依赖冲突
有同事在跑的时候报了NoClassDefFoundError: freemarker/template/TemplateException。查下来是工程里已经有一个低版本的FreeMarker,和screw-core需要的版本冲突了。解决办法是统一FreeMarker版本,或者排除掉传递依赖后手动引入指定版本。
<dependency> <groupId>cn.smallbun.screw</groupId> <artifactId>screw-core</artifactId> <version>1.0.5</version> <exclusions> <exclusion> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </exclusion> </exclusions> </dependency>然后单独引入你需要的FreeMarker版本。这种问题在集成类工具里太常见了,排查思路也不复杂:先看异常堆栈,再mvn dependency:tree看依赖树,找到重复的依赖后排除掉即可。
5.4 密码里有特殊字符的连接串问题
如果你数据库密码里包含&、?、#这类特殊字符,直接写在JDBC URL里会被解析成参数分隔符。遇到这种情况,最简单的办法是把密码单独放到setPassword里,而不是拼接在URL中。HikariCP本身就支持用户名密码分离,这也是我推荐用HikariCP而不是直接拼DriverManager的原因。
6. 把screw-core接入日常开发流程的实战思路
跑通代码方式和搞定几个坑之后,我已经能在本地随时生成数据库文档了。但一个人能生成不算什么,整个团队能用起来才叫落地。这一步我分成三阶段做。
6.1 阶段一:用Maven插件,让文档生成成为构建的一部分
如果想在项目构建时自动生成,screw官方提供了Maven插件。在pom.xml里配置好数据源和文档引擎,执行:
mvn screw:run就能在target目录下生成文档。配置和代码方式大同小异,只是把Java代码换成了XML。
我的建议是:不要把它绑定到package生命周期里强制所有人生成,否则只会拖慢构建。可以单独留一个docProfile,需要更新文档时手动执行,或者由负责发版的人在预发阶段跑一次。
6.2 阶段二:把注释规范写进开发约定
工具再强,也救不了没有注释的表。我后来给团队定的规矩很简单:
- 新建表必须有
COMMENT ON或建表语句里的表注释。 - 字段必须有COMMENT,枚举类字段要在注释里写明枚举含义。
- 中间表注释必须说明关联的双方和业务场景。
这不是什么新发明,但screw-core生成的文档会让这条规矩变得可检查。每次生成文档时,注释缺失的地方一眼就能看到,我直接把文档截图扔群里,比嘴上说一百遍都管用。
6.3 阶段三:和CI流水线结合起来自动归档
再往后,可以在CI里加一个定时任务或者手工触发任务:连接测试库,生成HTML和Markdown文档,用固定的文件名推送到一个内部文档站或者Git仓库的docs/database目录。
我实际用的是Git仓库方式。因为Markdown可以直接Diff,每次表结构变动,Git历史里都能看到文档变化,变相留下了一份数据库结构的演进记录。这个价值在回溯问题时非常明显。
7. 我用顺手之后的几点体会
如果你手头的项目也是几十张表起步,我真心建议花半小时把screw-core跑通,然后把生成文档的代码或插件配置放到项目里。它解决的不是“写文档”这一步,而是“文档永远落后于代码”这个常态问题。
几个小提醒:
- 第一次跑先用测试库,别直接连生产库。虽然它只读元数据,但连接池参数的误配还是可能带来压力。
- 生成Word时注意字体问题,一些Windows环境下中文字体看起来会发虚,HTML或MD更省心。
- 给输出目录一个固定路径,方便脚本或CI直接引用,别每次手动去target目录里翻。
我现在接手的每个新项目,落地第一周就会把数据库文档生成跑起来。表结构文档这事,确实不值得靠人肉去维护,交给工具之后,反而更快逼着团队把注释规范补起来。