用screw-core自动生成数据库表结构文档:从原理到实战
2026/9/11 15:12:40 网站建设 项目流程

上个月我接手一个内部管理系统,代码还没看,先栽在数据库上。项目里有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 简单说就是:连接数据库、读元数据、套模板、出文件

整个过程可以拆成四步:

  1. 你提供一个DataSource,它负责和数据库建立连接。
  2. screw-core调用JDBC的元数据接口,拿到所有表的字段、主键、索引、外键等信息。
  3. 数据被填充到FreeMarker或Velocity模板里。
  4. 引擎根据你指定的文件类型,输出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_configquartz相关表、临时表、日志表。全量生成会导致文档又长又没重点。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();

这几个配置的含义很清楚:按表名精确忽略、按表名前缀忽略、按表名后缀忽略。我实际生产环境里用的最多的是ignoreTablePrefixignoreTableSuffix,因为很多工程的临时表和日志表命名规律比较统一。反向的还有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目录里翻。

我现在接手的每个新项目,落地第一周就会把数据库文档生成跑起来。表结构文档这事,确实不值得靠人肉去维护,交给工具之后,反而更快逼着团队把注释规范补起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询