作为一名经常跟数据库和Java代码打交道的后端开发,我一直在找一个能直接节省时间的小工具。尤其是在拿到一张新表时,手动把 CREATE TABLE 语句翻译成实体类和 MyBatis 的 XML 映射,真的是一件既枯燥又容易出错的事。字段少还好说,字段一多,光是复制粘贴和改驼峰命名就能让人烦躁。所以我就花了一个周末,纯手写了一个 HTML 页面,实现了"MySQL 语句 → Java 实体类 + MyBatis 语句"的一键转换。这篇文章就围绕这个工具,完整拆解它的设计思路、核心实现、实操过程和踩坑记录,希望能给同样被这种重复劳动折磨的兄弟们一点启发。
先说说这个工具能干什么。你只需要把 MySQL 的建表语句(DDL)粘贴到页面左侧的文本域里,点一下"转换",右侧就会同时生成两份代码:一份是带 Lombok 注解、swagger 注解和 MyBatis-Plus 注解的 Java 实体类,另一份是可直接复制到 Mapper XML 文件里的 <resultMap>、<sql> 和基础 CRUD 语句。整个转换过程完全在浏览器本地完成,不需要安装任何环境,也不需要把数据传到服务器。工具的核心就是把"按规则生成代码"这件事自动化,规则是标准的,就不该靠人肉敲。
如果你是那种"能自动化就绝不手动"的开发者,或者你的团队里经常有人因为手工写 Mapper 而出低级错误,这篇文章应该能帮到你。我会从整体设计、核心解析逻辑、实体类生成、XML 生成、完整实操案例和常见问题几个方面,把整个工具的里里外外讲清楚。代码部分的思路你完全可以自己复刻一份,做成公司内部工具也好,个人效率工具也好,都行。
1. 工具整体设计与思路拆解
1.1 为什么选择纯 HTML 单页实现
这个工具我没有用后端服务,也没有用什么前端框架,就是纯 HTML + CSS + 原生 JavaScript,单文件搞定。为什么要这么干?很简单,作为一个开发者的个人工具,最重要的属性是"随时能用"和"不用维护"。
如果做成后端服务,意味着我要考虑部署、接口鉴权、服务器成本,还要担心数据安全性。虽然生成代码本身不是什么敏感操作,但对于一个给自己用的工具来说,这些基础设施成本完全不值得。如果做成 Webpack/Vue 项目,我得搭一套前端工程化环境,维护依赖版本,打包发布,这对一个几十行逻辑的单页应用来说也是杀鸡用牛刀。而一个纯 HTML 文件,双击就能在浏览器里打开,或者往公司内网一丢就能共享给同事,零成本、零依赖。
实际用下来,我个人更推荐把它定位成"团队内的轻量生产力工具"。你可以把 HTML 文件放进一个共享目录,或者用任何静态服务器托管,大家浏览器打开就能用。因为所有数据都在本地处理,不涉及传输,同事用起来也没有心理负担。这个思路本身也值得借鉴:不是所有工具都需要做成平台,很多解决痛点的场景,一个单页 HTML 就足够了。
1.2 功能模块划分与交互流程
整个页面可以分为三大块区域,这样设计是为了让使用者从视觉上就能理解转换逻辑,不需要看任何使用文档。
第一块是输入区,一个大号文本域,用于粘贴 MySQL 建表语句。这里我处理了一个细节:文本域默认放了一段示例 DDL,用户一打开页面就能看到效果。这种方式比"空页面 + 使用说明"的引导性强得多,用户直接点击转换就能看到产物,然后替换成自己的表结构就行。
第二块是配置区,我放了一排功能开关,包括"生成 Swagger 注解"、"生成 MyBatis-Plus 注解"、"使用 LocalDateTime"、"去掉逻辑删除字段"等选项。这些开关的本质是让工具适配不同团队的代码规范。比如有的老项目还在用 java.util.Date,有的新项目已经全面切换 LocalDateTime,有的团队严格禁止在实体类里用 Lombok,这些差异如果写死在代码里,工具的可复用性就大幅下降。所以我把这些"团队规范项"抽成了配置,默认值按主流新项目规范来设置。
第三块是输出区,我用 Tab 页签来分别展示"实体类"和"Mapper XML",每个页签下面有一个"复制"按钮和"下载"按钮。复制按钮用的是最新的 navigator.clipboard API,但对 file:// 协议下打开页面做了兼容处理——回退到 document.execCommand('copy')。下载按钮则是把生成内容包装成 Blob,以 .java 和 .xml 后缀下载,这两个小功能虽然不起眼,但实际使用频率非常高,直接在编辑器里粘贴要比来回选中复制方便得多。
1.3 技术选型:正则解析是核心,但不是全部
在动手写解析逻辑之前,我评估过几种实现方案。第一个想到的是用 JavaCC 或者 ANTLR 这类语法解析器,它们能从语法层面完全解析 MySQL DDL。但问题是,为了一个小工具引入这么重的解析器,而且还得把它编译成 JavaScript 版本,工程复杂度立刻就上去了。第二个方案是用第三方 SQL 解析库,比如 JSQLParser 的 JS 版本,生态和兼容性虽然不错,但会让单 HTML 文件变成多依赖项目。
最后我选择了"正则表达式 + 行级字符串处理"这个看似笨拙、但最实用的方案。为什么可行?因为建表语句的结构相对固定,字段定义和表属性的格式高度规律。我用正则拆解的关键思路是:先把表结构部分和表属性部分通过 ENGINE= 关键字切分开,再按逗号把字段定义逐条拆出来。每条字段定义再通过三个正则分别提取字段名、数据类型、注释和默认值。整个解析过程不追求 100% 覆盖所有语法变体,但能覆盖 95% 以上的日常建表语句。后面我会详细讲这个解析器的边界在哪里、哪些复杂情况它处理不了。
这里也想强调一个经验:做工具不要追求大而全,解决核心场景的 80% 需求已经能节省大量时间了,剩下的边缘 case 完全可以靠"生成后再微调"来处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实现:MySQL DDL 解析与类型映射
2.1 DDL 解析的整体思路
解析逻辑是工具的核心,我把它拆成了两个阶段。第一阶段是"预处理",主要工作是清洗输入文本:去掉多余空格、统一换行符、删除注释行和 SET、DROP TABLE、CREATE DATABASE 这类干扰语句,只保留 CREATE TABLE 后面的部分。第二阶段是"结构化提取",流程如下:
- 用正则
CREATE TABLE.*?\\((.*?)\\)提取括号内的内容,这个正则里的.*?是非贪婪匹配,避免跨表匹配。 - 将括号内的内容按逗号分割,得到每一条字段定义。这里有一个细节:字段定义里可能出现 DEFAULT
'a,b'这种带逗号的默认值,如果直接按逗号split会拆错。我的处理是先做一层"逗号保护",把默认值里的逗号临时替换为占位符,分割完成后再还原。 - 对每一条字段定义,分别匹配字段名、数据类型、是否主键、是否自增、是否非空、默认值、注释。
- 单独识别 PRIMARY KEY 行,用于处理复合主键的场景。
这里有一个很重要的取舍:我没有用 split('\\n') 按行处理,因为一条字段定义在极端情况下可能换行书写(比如每个字段占三行),虽然不常见,但容错性要考虑。所以"按逗号分割后再逐条解析"这个方案的鲁棒性更高。
2.2 MySQL 类型到 Java 类型映射表
类型映射是整个工具里最需要细心的地方,也是决定生成的实体类能否直接编译通过的关键。我整理了一张映射表,基本上是 Java 后端开发者的共识版本:
| MySQL 类型 | Java 类型 | 说明 |
|---|---|---|
| TINYINT(1) | Boolean / Integer | 配置项可选,默认 Integer |
| TINYINT | Byte / Integer | 字段注释含"是否/状态"时建议 Boolean |
| SMALLINT | Short / Integer | 按字段业务含义二选一 |
| MEDIUMINT | Integer | 不常见,但映射关系要写对 |
| INT / INTEGER | Integer | 基础映射 |
| BIGINT | Long | 主键最常见 |
| FLOAT | Float | 注意精度问题 |
| DOUBLE | Double | 基础映射 |
| DECIMAL / NUMERIC | BigDecimal | 必须是大数类型,后端必加 import java.math.BigDecimal |
| CHAR / VARCHAR | String | 无论长度,都是 String |
| TEXT / TINYTEXT / MEDIUMTEXT / LONGTEXT | String | 长度再大也只是 String |
| BLOB / BINARY / VARBINARY | Byte[] | 基础映射 |
| DATE | LocalDate / Date | 配置项可选 |
| DATETIME | LocalDateTime / Date | 配置项可选 |
| TIMESTAMP | LocalDateTime / Date | 和 DATETIME 的区别在于时区,Java 层面映射一致 |
| TIME | LocalTime / Time | 较少用,但也要支持 |
| JSON | String | 很多新项目把 JSON 直接用 String 接收 |
| BIGINT UNSIGNED | BigInteger | 这是容易踩坑的地方,后面会讲 |
| ENUM / SET | String | 建议映射为 String,具体枚举在业务层处理 |
这张表看下来似乎很简单,但其实坑藏在细节里。比如 TINYINT(1),在 MySQL 里它通常代表布尔值(比如 is_deleted 字段),所以很多生成器会直接映射成 Boolean。但如果你开发的系统是接老项目,老项目里 TINYINT(1) 的值可能存的是 0 和 1 之外的数字,直接 Boolean 映射就会出大问题。所以我的工具里默认把 TINYINT(1) 映射成 Integer,只有字段名或者注释里明确包含"是否""is"等标识时,才特殊处理成 Boolean。这种"聪明"的规则,恰恰是工具真正好用的原因。
再比如 DECIMAL,这个类型不映射成 BigDecimal 绝对会出事。如果按 Double 处理,金额字段在跨平台传输和计算时会出现精度丢失,这在支付、财务系统里是致命的问题。所以 DECIMAL 的映射规则在我的工具里是硬编码的,不允许用户通过配置修改,宁可生成的类多一个 import,也不能让精度问题埋到代码里。
2.3 字段注释提取与默认值处理
注释是生成代码时最容易被忽略、但实际价值最高的部分。我见过太多实体类里所有字段都没有注释,一个 private String a; 让后来接手的同事看得一头雾水。所以我的工具把 DDL 里的 COMMENT 指令提取出来,映射到 Java 字段的 Javadoc 注释上,同时映射到 XML 里 <resultMap> 的注释上。
提取注释的正则其实不复杂:COMMENT\\s*['\"](.*?)['\"]。但需要注意两点:一是注释内容本身可能包含单引号,比如 COMMENT '用户''s name',MySQL 里用两个单引号转义一个单引号,解析时要把 '' 还原成 '。二是注释里可能包含中文标点、括号等特殊字符,这些在生成 Javadoc 时如果不做处理,会导致注释不闭合。我的处理是,生成 Javadoc 时把注释里的 */ 替换成 * /,避免意外关闭注释块。
默认值处理相对简单,主要是为了生成 MyBatis 的 Insert 语句时使用。比如字段有默认值 CURRENT_TIMESTAMP 或者 DEFAULT 0,那么在生成 Insert 语句时,这个字段可以被排除在插入列表之外,让数据库自己去填充默认值。这个逻辑对应到 MyBatis-Plus 注解上就是 @TableField(fill = FieldFill.INSERT),但这里有个两难:如果依赖数据库默认值,那实体类字段上就不能写 @TableField(insertStrategy = FieldStrategy.NEVER),否则 MyBatis-Plus 会忽略这个字段的插入。这块逻辑我在工具里做了一个约定:有默认值的字段,默认不插入 Insert 语句;由 Java 代码主动赋值的字段(比如业务上维护的 create_time),由用户自己在生成后手动调整。毕竟工具负责 80% 的自动生成,剩下 20% 的定制化还是得人来判断。
3. 实体类生成:下划线转驼峰与注解体系
3.1 下划线命名转驼峰命名的实现
Java 的命名规范里,类名用大驼峰(PascalCase),字段名用小驼峰(camelCase),而数据库表的命名习惯是下划线分割(snake_case)。所以表名 user_info 要转换成类名 UserInfo,字段名 last_login_time 要转换成 lastLoginTime。这个转换逻辑是整个工具的"门面",如果转换错一个字符,生成的代码就全部作废。
转换的算法本身很简单:以下划线为分隔符拆分成单词,首单词全小写(字段名),后续单词首字母大写。但实战中有几个特殊情况要处理:
- 连续多个下划线:比如
user__name,这在脏数据表里可能存在。我的处理是合并多个下划线为一个分隔符,避免生成空单词导致命名断裂。 - 数字开头的字段名:比如
1st_priority,这在业务表里偶有出现。Java 变量名不能以数字开头,我的处理是自动在前面加一个field前缀,生成field1stPriority,虽然不好看但至少能编译。 - 全部大写的字段名/表名:有些老系统表名是大写的
USER_INFO,转换前需要先统一转小写,再走驼峰转换。 - 已经是驼峰命名的字段:比如
userName,如果按_拆分,它只会拆成一个单词,那保持原样即可,这在老 MySQL 库中不算罕见。
这里给一个转换函数的参考实现:
javascript复制function toCamelCase(name, upperFirst) {
if (!name) return '';
let parts = name.toLowerCase().split('_').filter(p => p.length > 0);
let result = parts.map((p, idx) => {
if (idx === 0) return p;
return p.charAt(0).toUpperCase() + p.slice(1);
}).join('');
if (upperFirst) {
result = result.charAt(0).toUpperCase() + result.slice(1);
}
return result;
}
这个函数虽然只有几行,但它承载的是后端开发最基础的命名规范共识。在工具里,我把 upperFirst 这个参数用在类名生成上,字段名生成时传 false,表名生成类名时传 true,其它场景都复用同一个函数。
3.2 主键识别与 MyBatis-Plus 注解
主键在实体类和 XML 里都是"特殊公民"。在 DDL 中,主键可以通过两种方式定义:一是在字段行里直接加 PRIMARY KEY,二是表底部单独写一行 PRIMARY KEY (id)。我的工具两种都支持,但优先级有讲究:如果某个字段行内声明了主键,那底部声明的那条会被忽略,以防重复生成主键标记。
映射到 MyBatis-Plus 注解时,主键字段会加上 @TableId。这里我还做了一个更细的判断:如果字段行里有 AUTO_INCREMENT,那么主键类型标记为 IdType.AUTO,否则标记为 IdType.INPUT。这个区别非常重要——自增主键在插入时应该不传 id,让数据库生成;非自增主键(比如分布式 ID)在插入时必须显式赋值,否则插入会报主键为空。
实际上这个细节很容易被新手遗漏。很多人写完实体类,跑插入发现 "Field 'id' doesn't have a default value" 报错,就是因为 MyBatis-Plus 默认把主键当成 ASSIGN_ID(雪花算法)处理,而数据库里实际是自增主键。所以生成的 @TableId(type = IdType.AUTO) 这一行,在多数场景下能帮使用者避免一个非常隐蔽的 bug。
非主键字段统一打 @TableField 注解,但只有当列名和字段名不完全一致时才显式写出 value 属性。比如字段 lastLoginTime 对应列 last_login_time,就需要写 @TableField("last_login_time")。如果列名正好和字段名一致(比如列 name 映射字段 name),就不重复写注解,让 MyBatis-Plus 走默认映射规则,这也能让实体类更清爽。
3.3 Lombok、Swagger 与序列化支持
现在的 Java 后端项目,实体类基本标配 Lombok 和 Swagger 注解。在工具里,我通过配置开关决定是否生成这些注解。Lombok 部分默认生成 @Data,并且如果类里包含非静态字段,则额外生成 @Accessors(chain = true)。为什么默认加 @Accessors(chain = true)?因为链式调用 new User().setName("张三").setAge(18) 在现代业务编码里太常见了,它能简化很多代码。但这是一个团队代码规范问题,如果团队不认链式调用,把这两个注解删掉或者调整配置即可。
Swagger 注解方面,默认给每个字段生成 @ApiModelProperty(value = "用户名", example = "zhangsan")。这里需要说明的是,注释里提取的描述会放在 value 属性里,而 example 属性我留空或者使用默认值。为什么不是把 example 也自动填上?因为工具没有能力凭空猜出字段的合理示例值,宁可留空也不误导调用方。
序列化方面,我默认让实体类实现 Serializable 接口,并生成 private static final long serialVersionUID = 1L;。这是一个老生常谈但又非常必要的习惯——尤其是当你的实体类会经过消息队列、Redis 缓存或者 Dubbo RPC 传输时,不实现序列化接口的代价是巨大的。有的团队可能觉得"加不加无所谓,反正大多数情况下 JVM 会自动处理",但等你真的遇到 NotSerializableException 时再来补,那可就晚了。
3.4 import 管理的细节
生成实体类时,import 列表也不能出错。String 和 Integer 这些 java.lang 包下的类型不需要显式 import,但 BigDecimal、LocalDateTime、LocalDate、LocalTime、BigInteger、Date、List(如果字段类型有集合)等,必须按需生成。我在工具里是"用集合记录类型,最后统一排序输出",确保不重复、不乱序、不遗漏。
这里有一个偷懒技巧:我可以先把所有可能用到的 import 全部打出来,不用的就多几行注释而已,Java 编译器也不会报错。但这样生成的代码不干净,有经验的人一眼就能看出来是工具生成的。所以我的实现是"动态去重收集 import",生成完实体类主体之后,再在头部统一拼装 import 块。这个细节虽然小,但直接决定了生成代码的专业度。
4. MyBatis XML 生成:ResultMap 与 SQL 语句
4.1 ResultMap 映射规则
生成 XML 的第一步是生成 <resultMap>。它有一个最基本的映射结构:<id> 标签对应主键列,<result> 标签对应普通列。column 属性写数据库列名,property 属性写实体类字段名,jdbcType 属性写数据库对应的 JDBC 类型。三个属性一个都不能少,少了后运行期容易出问题。
很多人在写 MyBatis 的 resultMap 时,jdbcType 经常写错或者干脆不写。不写 jdbcType,在大多数情况下 MyBatis 能自动判断,但在某些数据库驱动(比如 Oracle 的驱动,或者 MySQL 处理 null 值插入时)会报 JDBCType 相关的错误。为了稳妥,我把 jdbcType 的映射规则也做成了一张表:VARCHAR 对应 VARCHAR,INT 对应 INTEGER,BIGINT 对应 BIGINT,DATETIME 对应 TIMESTAMP,DECIMAL 对应 DECIMAL,TEXT 对应 LONGVARCHAR,BLOB 对应 BLOB 等。这些映射关系在 MyBatis 官方文档里都能找到依据。
ResultMap 的 type 属性值需要是一个完全限定类名。这里我在工具里增加了一个"包名"配置项,让用户填写实体类的包名前缀,比如 com.example.entity,生成时自动拼成 com.example.entity.UserInfo。这样使用者复制 XML 到项目里时,不需要手动改包名路径,一步到位。如果没有填包名,工具会退化为使用简短类名,并提醒用户手动补全。
4.2 列清单与 SQL 片段设计
在 MyBatis 的 Mapper XML 里,我生成了两个最常用的 SQL 片段:<sql id="Base_Column_List"> 和 <sql id="Base_Column_List_NoId">。前者包含主键列和所有普通列,后者去掉了主键列,用于 Insert 语句的列清单。这个设计看似简单,却能在后续的 CRUD 语句里大量复用,避免 SQL 里到处写一长串列名。
至于为什么 Insert 要用"不带主键"的列清单,前面已经说过——自增主键不应该出现在 Insert 语句里。为了让生成的代码更可靠,我在这里做了一个额外判断:如果主键字段的 DDL 里没有 AUTO_INCREMENT,则 Insert 语句使用完整列清单,并显式插入主键值。这一点是很多自动生成器容易忽略的逻辑分支。
列清单的拼接方式不是简单的 A, B, C,而是每一列后面接一个 #{} 占位符,并显式标注 jdbcType。例如:
xml复制<sql id="Base_Column_List">
id, username, password, email, avatar, status, balance,
last_login_time, create_time, update_time, deleted
</sql>
这个片段在 selectByPrimaryKey、selectList、updateByPrimaryKeySelective 中会被 <include refid="Base_Column_List" /> 引用。把列定义集中管理,后续维护表结构变化时只需要改这一个地方。这种设计思路是 MyBatis 项目实践中最常见的做法,工具只是帮你把这个最佳实践固化了。
4.3 基础 CRUD 语句生成规则
工具默认生成五条 SQL 语句:selectByPrimaryKey、selectByCondition、insert、updateByPrimaryKeySelective、deleteByPrimaryKey。其中 selectByCondition 是我额外加的,因为实际业务里按照某几个条件查询是最高频的场景,只生成主键查询根本不够用。
selectByPrimaryKey:SELECT <include Base_Column_List> FROM 表名 WHERE 主键 = #{主键}。selectByCondition:只生成一个基础查询模板,查询条件留空并加注释提示,让使用者自行补充动态<where>条件。insert:使用<trim>动态拼接非空字段,这样只插入有值的字段。对应数据库层面,没有值的字段走默认值。updateByPrimaryKeySelective:同样使用<set>标签配合<if test="字段 != null">,实现只更新非空字段的动态更新。这里有一个细节,更新语句的条件永远只针对主键,并且主键字段本身不出现在<set>中。deleteByPrimaryKey:DELETE FROM 表名 WHERE 主键 = #{主键}。如果配置了逻辑删除字段,这条语句会改造成UPDATE 表名 SET 逻辑删除字段 = 1 WHERE 主键 = #{主键}。
每个 SQL 的 ID 命名都遵循了 MyBatis 官方约定和 MyBatis-Plus 的规范,「selectByPrimaryKey」这种命名在 Spring Boot 项目里可以通过 MyBatis 的 mapper 接口方法名直接对应,不需要额外写注解。如果你想简化,生成后手动调整方法名即可。
5. 完整实操:从 DDL 到代码的一键生成
5.1 现场演示:用户订单表 DDL 转换
理论说了那么多,不如直接演示一条真实业务表的转换过程。假设我拿到的是下面这张用户订单表的 DDL,它包含了整数、字符串、小数、时间、逻辑删除等多种字段类型,覆盖面比较有代表性:
sql复制CREATE TABLE `t_order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
`total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
`status` TINYINT(4) DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已取消',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT(1) DEFAULT 0 COMMENT '逻辑删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订单表';
把这段 DDL 粘贴到工具的输入框,点击"转换"后,生成的实体类核心代码示意如下(节选关键部分):
java复制@Data
@Accessors(chain = true)
@TableName("t_order")
public class TOrder {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private Integer status;
private String remark;
private LocalDateTime payTime;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
这里有几个细节值得注意:表名 t_order 转换成了类名 TOrder,而 order_no 转换成了 orderNo,total_amount 转换成了 totalAmount,全部按驼峰规范处理了。DECIMAL 正确映射成 BigDecimal,DATETIME 正确映射成 LocalDateTime,逻辑删除字段 deleted 加上了 @TableLogic 注解。这些都是工具默认配置下的输出。
生成的 XML 部分,resultMap 的 column 和 property 一一对应,Base_Column_List 里列名顺序和 DDL 保持一致,Select 语句、Insert 语句都使用了动态 SQL 标签。整体效果基本"开箱即用",复制到项目里就能跑。
5.2 从 MySQL Workbench 导出表结构的小技巧
实际使用时,你不可能每次都手敲 DDL,大部分情况下 DDL 是从数据库客户端工具里导出的。这里分享一个我自己的操作习惯:在 MySQL Workbench 中,右键目标表,选择 "Copy to Clipboard" → "Create Statement",就能把建表语句直接复制到剪贴板。在 Navicat 里则可以通过"对象信息"面板查看建表 SQL,再手动复制。
但这里有一个常见的坑:从客户端工具复制出来的 DDL,往往带有 DROP TABLE IF EXISTS 或者 SET FOREIGN_KEY_CHECKS = 0; 这类额外语句。直接把这样的 DDL 粘贴到工具里,正则解析时会忽略这些干扰行——我在预处理阶段已经做了过滤,所以一般不会出问题,但如果你自己写类似的解析器,一定要记得处理。另外,如果复制出来的是 INSERT INTO 语句,那工具是无法处理的,因为它是全自动的,只认 CREATE TABLE。
5.3 生成代码集成到 Spring Boot 的两种方式
生成完实体类和 XML 后,接入 Spring Boot 项目有两种主流方式。如果你的项目是原生 MyBatis(没有用 MyBatis-Plus),需要把实体类放到对应包下,把 XML 文件放到 resources 的 mapper 目录下,并在 application.yml 里配置 mybatis.mapper-locations: classpath:mapper/*.xml。同时还要保证 resultMap 的 type 属性指向的包名和实际实体类包名一致,这一步最容易出错。
如果你的项目用的是 MyBatis-Plus,处理要更简单一些。实体类上的 @TableName 和 @TableId 注解已经帮你完成了实体与表的映射,XML 文件只是补充复杂查询时用。MyBatis-Plus 的 BaseMapper 提供了基础的 CRUD 方法,XML 里主要保留 selectByCondition 这类业务查询。我的工具在生成时也考虑了这一点,生成的 XML 文件是完全兼容 MyBatis-Plus 项目的,不会产生冲突。
6. 常见问题与排查技巧实录
6.1 类型映射引发的"编译不过"问题
我在实际测试工具时,遇到的最多的反馈类型是开发者在迁移老项目时,生成的实体类编译不过。排查下来,大多数情况是 DECIMAL 没有正确引入 BigDecimal,或者 DATETIME 映射成了 java.util.Date 但配置里其实选了 LocalDateTime。其实这类问题大多不是工具 bug,而是使用者没有先看配置项。
有个建议:使用工具前,先花十秒钟确认三件事。第一,项目用 Date 还是 LocalDateTime;第二,要不要 Lombok;第三,表字段里有没有 DECIMAL、BIGINT UNSIGNED 这种特殊类型。确认完之后再点转换,基本一次到位。如果你发现生成的代码里 import 不完整,不要急着骂工具,先检查是不是输入 DDL 里包含了不完整的字段定义(比如某些客户端工具导出时会截断注释),这也是实际遇到过的 case。
6.2 复合主键与联合唯一索引的处理
复合主键是工具的一个边界情况。单主键的解析非常简单,但遇到 PRIMARY KEY (id, user_id) 这种联合主键时,工具如何识别?我的处理是:在生成实体类时,复合主键的每个字段仍然标注为普通 @TableField,同时额外生成一个起提示作用的 Javadoc:// 复合主键,部分字段为联合主键的一部分,请注意业务唯一性。这样做是为了避免在实体类上标注多个 @TableId,因为 MyBatis-Plus 的 @TableId 本身不支持多主键。
在 MyBatis XML 生成时,复合主键条件下的 selectByPrimaryKey 会变成 WHERE id = #{id} AND user_id = #{userId} 的多条件查询,updateByPrimaryKeySelective 也会把两个主键都作为条件。这个逻辑我在工具里做了完整的支持,但说实话,复合主键在互联网公司的业务表里已经越来越少见,大多数场景都是单主键 + 唯一索引的组合。
6.3 反引号、特殊字符与保留字的处理
MySQL 建表语句里经常出现反引号,比如 `order`、`desc`。反引号本身是 MySQL 的引用符号,用于避免字段名和保留字冲突。在工具解析时,如果字段名带反引号,正则匹配到的内容会包含反引号,直接拿去生成 Java 字段名可能在 static、class 这种保留字上出问题。我的处理是:解析字段名时自动去掉反引号,然后检查是否是 Java 保留字,如果是则在生成字段名时加后缀 Field(如 order → orderField),确保生成的代码能编译。
另外,有些 DDL 里字段名含有中文,比如 `姓名` VARCHAR(20),这在一些老的内网系统里确实存在。工具会保留中文字段名,但在生成 Java 字段时强制转换为拼音缩写并不现实,所以遇到这种情况我会在结果区显示警告信息,提醒用户手动修改。自动生成工具的边界就在这里,识别到异常场景比盲目生成一堆错误代码更有价值。
6.4 不要忽略"生成后的人工微调"
这个工具说到底是一个"代码生成器",它解决的是 80% 的机械劳动,而不是 100% 的业务逻辑。比如 UPDATE 语句的更新策略、某些字段的插入策略、复杂业务查询的 <where> 条件、数据权限过滤等,这些仍然需要人来写。我有一个经验:生成代码后,先编译一次,再在 IDE 里全局搜索一下 TODO 或 WARNING 注释,把工具标记出来需要人工确认的地方过一遍。这样既享受了自动化带来的效率,又规避了盲目信任生成代码带来的风险。
这个经验不仅适用于这个工具,任何代码生成器都适用。自动化生成的代码是"脚手架",而真正的业务逻辑、异常处理、事务控制,还是得靠人来思考。工具帮我把时间从重复劳动里省出来,省出的时间就花在这 20% 的人工微调上,这其实是性价比最高的分工方式。
7. 一些想补充的实践经验
用这个工具写代码写了几个月,我自己的最大感受是:不要小看一个"小工具"对开发效率的影响。以前每接一张新表,从看 DDL 到写实体类、写 Mapper,快的话也要十分钟,慢的话遇到字段多表结构乱的,二十分钟都打不住。现在用工具生成,从粘贴 DDL 到代码落盘,三分钟搞定。一天哪怕只转换三张表,省下来的时间也足以让人感觉明显。
如果你决定把这个工具引入团队,有一点可以优化:把工具里的配置项通过 URL 参数传递。比如 index.html?useLocalDateTime=true&useSwagger=false,这样不同项目组的同事可以把自己的偏好保存成浏览器书签,打开就是适合自己的配置。这个小功能实现很简单,但在团队推广里很加分。
另外,这个工具还能继续扩展。比如把生成目标扩展到 Kotlin 数据类、MyBatis-Plus LambdaQueryWrapper 的查询条件、DTO/VO 的字段拷贝方法等。这些扩展方向都是顺着"从表结构到代码"这条主线往下走的,不会破坏原有框架。我在自己的版本里已经增加了"生成 VO"的开关,输出产物是一个去掉 @TableId 注解、去掉逻辑删除字段的轻量实体类。这个扩展对于做接口返参时非常实用,避免了把 deleted 这种内部字段直接暴露给前端。
其实从技术实现上来讲,这类"规则驱动代码生成"的工具没有多高深的门槛,核心就是解析规则 + 模板拼接。真正让它值钱的地方在于对业务场景的理解——知道哪条规则在什么场景下适用,知道生成什么样的代码才是团队真正需要的。写这个小工具的过程,也让我重新梳理了一遍自己在 MyBatis 使用上的最佳实践,算是一举两得。如果你也经常被各种"无脑但耗时"的代码任务占据时间,与其硬扛,不如花一个周末写个小工具,让机器去干机器的活。
