项目里但凡要接一张新表,最快的方式就是用 IDEA 连接数据库之后,直接把表结构反向生成实体类。以前我一直靠自带的一键生成功能,生成完再手动补 Spring Boot JPA 注解、加 Lombok、调整数据库类型映射,后来发现只要表一多,这种“半自动”操作就会变成纯粹的手工流水线,既重复又容易漏注解。干脆花点时间研究了一下用 Groovy 脚本定制生成 Spring Boot JPA 实体类的方案,把注解、注释、类型转换这些重复劳动全部在生成阶段一次性解决。这篇文章我就把从数据库连接配置、脚本编写到实际生成的完整思路和可复用脚本分享出来,适合正在用 JPA、Hibernate,又每天被建表改表折腾的同学参考。
1. 直接用 IDEA 自带功能生成实体类,到底差在哪
1.1 自带生成器的输出模式
IDEA 的数据库工具面板确实带了一套“Generate POJOs.groovy”和“Generate JPA entities.groovy”脚本,操作路径很直观:在 Database 面板选中表,右键 -> Generate,然后选对应脚本就行。第一次用的时候你会觉得:这不就能生成实体类了吗,还要折腾什么?
但实际生成出来的东西,离项目能用还差得远。默认脚本生成的 Java 类:
- 只输出私有字段和 Getter/Setter 方法,不会自动加入 Lombok 注解;如果你的团队习惯用 Lombok,每个类都得手工再删掉全套 Getter/Setter,加上
@Getter @Setter。 - 类上没有
@Entity和@Table注解,或者有@Entity但表名策略不一定符合你的命名习惯,比如数据库里明明是sys_user,生成出来类名可能叫SysUser但注解里的name没有正确映射。 - 数据库字段上的注释几乎不会自动转换成代码里的 JavaDoc 注释,你得到的就是一堆裸字段。
- 类型映射过于保守,PostgreSQL 的
timestamptz可能直接映射成Object或者Timestamp,numeric可能变成BigDecimal还好,int8可能映射成BigInteger,这些都不是 Spring Boot JPA 项目里最顺手的类型。
另一个问题是,自带脚本默认不读取主键信息生成@Id和@GeneratedValue。生成出来的类里所有字段都一样,主键字段也需要你手工标注。一次两次还好,表一多就特别容易漏。
1.2 Spring Boot JPA 项目对实体类的真实需求
Spring Boot 项目里的实体类,早就不是一个简单的 POJO 了。对照我们日常项目的代码规范,一个合格的 JPA 实体类至少要有下面这些东西。
| 需求点 | 说明 |
|---|---|
| 类注解 | @Entity+@Table(name = "xxx"),必要时指定 schema |
| 主键策略 | @Id+@GeneratedValue(strategy = GenerationType.IDENTITY)或SEQUENCE |
| 字段注解 | @Column(name = "xxx"),字段名与数据库列名严格对应 |
| Lombok 注解 | 类上@Getter @Setter,彻底简化 Getter/Setter |
| 注释保留 | 表注释和列注释要变成 JavaDoc,方便后面同事接手 |
| 时间类型 | timestamp/timestamptz映射为LocalDateTime,而非Date |
| 数值类型 | numeric映射为BigDecimal,避免精度丢失 |
| 序列化支持 | 实现Serializable并定义serialVersionUID |
这些需求里,除了前两项,其余几乎都要靠手工补。而 Groovy 生成脚本最大的价值,就是把这些“规范化动作”固化成模板,每次生成都按同一个标准执行,不会因为人的状态不同而出现遗漏。
2. 准备阶段:正确的数据库连接与脚本加载方式
2.1 在 IDEA 中配置数据库数据源
要生成实体类,第一步一定是把数据源拉通。IDEA 的 Database 面板本身就是完整的数据源客户端,支持 PostgreSQL、MySQL、SQL Server、Oracle 等主流数据库。以 PostgreSQL 为例,配置步骤如下:
- 打开右侧 Database 面板,点击
+,选择 Data Source -> PostgreSQL。 - 填写 Host、Port、Database、User、Password。
- 点 Test Connection 测试连接。
- 如果提示缺少驱动,让 IDEA 自动下载即可。不过我建议有条件的话使用官方 JDBC 驱动版本,部分场景下 IDEA 内置驱动的版本策略可能和你本地的数据库版本有一定的兼容差异。
- 连接成功后,展开对应 schema 就能看到表。
这里有两个很容易踩的坑。第一个是 schema 选择,PostgreSQL 默认连的是publicschema,如果你的业务表放在ods、app之类的 schema 下,连接配置里的 Schema 标签页一定要手动勾选,否则列表里看不到表。第二个是时区参数,MySQL 连接时经常遇到“serverTimezone”相关的报错,需要在连接 URL 上追加useSSL=false&serverTimezone=Asia/Shanghai参数;PostgreSQL 一般不用额外处理,但如果你所在网络环境有代理或防火墙,驱动下载失败时也可以手动把 jar 包放到 IDEA 的驱动目录里。
配置数据源这件事,看起来简单,其实决定后面脚本能不能顺利读取列信息和注释。IDEA 生成脚本时,需要数据源保持在线状态,并已同步过表结构元数据。如果数据源处于离线状态或者刷新失败,脚本拿到的列信息可能是空的,生成结果自然残缺。
2.2 Groovy 脚本存放位置与调用入口
很多人不知道 IDEA 的脚本自定义能力在哪里配置。IDEA 默认自带几个 Groovy 脚本模板,但它们存放在 IDEA 全局扩展目录里,不推荐直接改,更好的做法是把你的自定义脚本放到一个专门目录。
设置路径在Settings -> Tools -> Database -> Groovy Scripts Directory,点开之后可以配置脚本目录。我习惯把目录指向项目仓库里的tools/db-codegen文件夹,同时也放入 Git 管理,这样团队里其他成员同步代码后,就能在各自的 IDEA 里使用同一套生成脚本。
脚本放好后,调用方式有两种:
- 在 Database 面板选中表,点击右键,选择
Generate POJOs.groovy(或你自定义的脚本文件名)。 - 如果脚本不在默认命名范围内,可以在右键菜单里选
Scripted Extensions -> Go to Scripted Extensions,直接检查当前生效的脚本列表。
需要提醒的是:自定义脚本必须写在 IDEA 认可的目录里,并且脚本名后缀是.groovy,IDEA 才会在右键菜单里识别到。脚本本质上是一段运行在 JetBrains 脚本引擎上的 Groovy 代码,它可以直接拿到当前选中表对象、数据库列对象、数据类型对象等上下文,所以在右键菜单里选中表运行脚本是前提条件,直接当作普通 Groovy 文件运行是没有意义的。
3. 定制 Groovy 脚本的三处关键代码
3.1 表名/字段名与 Java 命名规则的转换
数据库里绝大多数表都是下划线风格,比如sys_user_info、create_time,而 Java 类名应该是SysUserInfo,字段名应该是createTime。Groovy 脚本里的命名转换函数就要解决这个问题。
实现思路很简单,按下划线拆开,第一个单词首字母小写,后续单词首字母大写,拼接起来就是驼峰格式。类名再额外把第一个单词首字母大写。
def toCamelCase(String name) { def parts = name.split("_") def result = parts[0].toLowerCase() for (int i = 1; i < parts.length; i++) { result += parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1).toLowerCase() } return result } def toPascalCase(String name) { def camel = toCamelCase(name) return camel.substring(0, 1).toUpperCase() + camel.substring(1) }这段代码处理常规场景完全够用。要注意两个例外情况。一是表名里如果没有下划线且全是小写,toCamelCase会原样返回,类名能正常首字母大写;二是表名里带有特殊前缀(比如t_sys_user),你会发现生成出来的类名变成了tSysUser,这对很多项目来说不是想要的。可以在脚本里增加替换数组,把t_、tb_、tab_等固定前缀在转换前去掉。
def cleanTableName(String name) { def lower = name.toLowerCase() if (lower.startsWith("t_")) return name.substring(2) if (lower.startsWith("tb_")) return name.substring(3) return name }3.2 数据库类型到 Java 类型的映射表
这一块是整个脚本的重点。Spring Boot JPA 项目里,类型映射直接影响后续仓库接口的定义和业务代码的写法。PostgreSQL 数据库里我用过的类型基本都可以归类成下面这张表。
| 数据库类型 | Java 类型 | 理由 |
|---|---|---|
| int2 / int4 | Integer | 常规整型 |
| int8 / bigserial | Long | 主键 ID 基本都是 Long |
| varchar / char / text / citext | String | 字符串场景最多 |
| numeric / decimal | BigDecimal | 金额、精度计算必须用 BigDecimal |
| float4 | Float | 单精度浮点 |
| float8 | Double | 双精度浮点 |
| bool / boolean | Boolean | 布尔字段 |
| date | LocalDate | Java 8 时间类型 |
| timestamp / timestamptz | LocalDateTime | 业务时间字段首选 |
| time / timetz | LocalTime | 时分秒场景 |
| bytea | byte[] | 二进制文件存储 |
| uuid | UUID | PG 原生 uuid 类型 |
| json / jsonb | String | 文本方式存取,复杂转换交给应用层 |
为什么numeric要固定映射成BigDecimal?因为浮点数会有精度问题,金额字段一旦出现精度偏差,后面查问题特别痛苦。为什么timestamp用LocalDateTime而不是Date?Hibernate 5.2 之后对 Java 8 时间类型支持已经很成熟,LocalDateTime更符合现在的开发习惯,并且不存在时区混淆问题。jsonb字段在这张表里映射成String是一个兜底方案,因为 JPA 标准里没有直接对应 JSON 类型的字段注解,实际项目里如果需要完整支持 JSON 查询,一般会在 Hibernate 6 里通过@JdbcTypeCode(SqlTypes.JSON)来实现,这就属于实体类生成后的定制优化了。
映射函数的代码可以这样写:
def mapType(DataType dataType) { def typeName = dataType.name?.toLowerCase() switch (typeName) { case ["int", "int2", "int4"]: return "Integer" case ["int8", "bigint", "bigserial"]: return "Long" case ["varchar", "char", "bpchar", "text", "citext"]: return "String" case ["numeric", "decimal"]: return "BigDecimal" case ["float4", "real"]: return "Float" case ["float8", "double precision"]: return "Double" case ["bool", "boolean"]: return "Boolean" case ["date"]: return "LocalDate" case ["timestamp", "timestamptz", "datetime"]: return "LocalDateTime" case ["time", "timetz"]: return "LocalTime" case ["bytea"]: return "byte[]" case ["uuid"]: return "UUID" default: return "String" } }如果你用的是 MySQL,那么需要注意把datetime对应到LocalDateTime,tinyint(1)对应到Boolean,decimal对应到BigDecimal,longtext对应到String。映射表完全可以按团队实际规范继续扩充。
3.3 注解模板与文件头生成逻辑
脚本的核心输出是注解。项目里实体类的注解有两层,一层是类注解,一层是字段注解。
类注解固定输出:
@Getter @Setter @Entity @Table(name = "sys_user") public class SysUser implements Serializable { private static final long serialVersionUID = 1L; }字段注解按字段类型区分:主键字段输出@Id和@GeneratedValue(strategy = GenerationType.IDENTITY);普通字段输出@Column(name = "xxx");字段有注释时,注释放在 JavaDoc 里。
这块脚本逻辑可以抽象成下面几行:
def appendFieldAnnotations(StringBuilder sb, DasColumn column, boolean isPk) { if (column.comment) { sb.append(" /**\n") sb.append(" * ").append(column.comment).append("\n") sb.append(" */\n") } if (isPk) { sb.append(" @Id\n") sb.append(" @GeneratedValue(strategy = GenerationType.IDENTITY)\n") } sb.append(" @Column(name = \"").append(column.name).append("\")\n") }关于主键生成策略,这里有个点要特别说明。PostgreSQL 里如果主键用的是identity自增列,IDENTITY是合理的;但如果你的表用了序列(sequence),比如nextval('sys_user_id_seq'),那实体类里应该改写成:
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sys_user_id_seq") @SequenceGenerator(name = "sys_user_id_seq", sequenceName = "sys_user_id_seq", allocationSize = 1)所以脚本里如果把主键策略写死成IDENTITY,就会和某些项目的数据库设计冲突。我的建议是把主键策略作为脚本顶部的常量,生成前按项目改一行就行。
字段注解里的@Column(name = "xxx")还有一个细节:如果数据库列名和 Java 字段名的驼峰转换关系是严格可逆的,比如列create_time转成createTime,Hibernate 开启命名策略后其实可以不用写name。但我觉得显式写上更安全,因为有些历史表字段命名不规律,比如dbsj(更新时间缩写)、bz(备注缩写),如果不写name,Hibernate 默认就会去找bz对应的驼峰字段,结果就是字段映射错乱。
4. 完整脚本:可直接复制使用的 Groovy 生成器
4.1 脚本全文与配置项说明
下面这个脚本是我在 IDEA 2023.3 版本上验证过的框架,你使用时可能需要根据 IDEA 版本对 API 导入做微调,但整体逻辑是完整可参考的。脚本文件可以命名为GenerateJpaEntityForSpringBoot.groovy,放入前面配置的 Groovy Scripts Directory 后,在表上右键就能调用。
import com.intellij.database.model.DasTable import com.intellij.database.model.DataType import com.intellij.database.util.DasUtil import com.intellij.database.psi.DasColumn import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter // ================= 可配置项 ================= String PACKAGE_NAME = "com.example.jpa.entity" String AUTHOR = "your-name" boolean USE_LOMBOK = true boolean ENABLE_SERIALIZABLE = true String ID_STRATEGY = "IDENTITY" // 可选 IDENTITY / SEQUENCE String SEQ_NAME = "" // ID_STRATEGY == SEQUENCE 时必填 // ========================================== def targetDir = FILES.chooseDirectory("选择实体类保存目录", "src/main/java") if (targetDir == null) { println "未选择目录,已取消" return } def selectedTables = SELECTION.getTables() if (selectedTables == null || selectedTables.isEmpty()) { selectedTables = [table] } if (selectedTables == null || selectedTables.isEmpty()) { println "未获取到表,请先在 Database 面板选中一张或多张表" return } selectedTables.each { dasTable -> def className = toPascalCase(dasTable.name) def imports = new LinkedHashSet<String>() def fields = new StringBuilder() if (ENABLE_SERIALIZABLE) { imports.add("java.io.Serializable") } imports.add("javax.persistence.*") if (USE_LOMBOK) { imports.add("lombok.Getter") imports.add("lombok.Setter") } DasUtil.getColumns(dasTable).each { col -> def fieldName = toCamelCase(col.name) def javaType = mapType(col.dataType) def isPk = isPrimaryKey(dasTable, col) if (javaType.startsWith("java.")) { imports.add(javaType) } if (javaType == "BigDecimal") { imports.add("java.math.BigDecimal") } if (javaType == "LocalDate") { imports.add("java.time.LocalDate") } if (javaType == "LocalDateTime") { imports.add("java.time.LocalDateTime") } if (javaType == "LocalTime") { imports.add("java.time.LocalTime") } if (javaType == "UUID") { imports.add("java.util.UUID") } if (col.comment) { fields.append(" /**\n") fields.append(" * ").append(col.comment).append("\n") fields.append(" */\n") } if (isPk) { fields.append(" @Id\n") if (ID_STRATEGY == "SEQUENCE") { fields.append(" @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = \"").append(SEQ_NAME).append("\")\n") imports.add("javax.persistence.SequenceGenerator") fields.append(" @SequenceGenerator(name = \"").append(SEQ_NAME).append("\", sequenceName = \"").append(SEQ_NAME).append("\", allocationSize = 1)\n") } else { fields.append(" @GeneratedValue(strategy = GenerationType.IDENTITY)\n") } } fields.append(" @Column(name = \"").append(col.name).append("\")\n") fields.append(" private ").append(javaType).append(" ").append(fieldName).append(";\n\n") } def packagePath = PACKAGE_NAME.replace('.', '/') def saveDir = new File(targetDir, packagePath) saveDir.mkdirs() def file = new File(saveDir, className + ".java") file.write(buildClassContent(className, dasTable, fields.toString(), imports)) println "已生成: ${file.path}" } def buildClassContent(String className, DasTable table, String fields, Set<String> imports) { def sb = new StringBuilder() sb.append("package ").append(PACKAGE_NAME).append(";\n\n") imports.each { imp -> sb.append("import ").append(imp).append(";\n") } sb.append("\n") if (table.comment) { sb.append("/**\n") sb.append(" * ").append(table.comment).append("\n") sb.append(" * @author ").append(AUTHOR).append("\n") sb.append(" */\n") } if (USE_LOMBOK) { sb.append("@Getter\n") sb.append("@Setter\n") } sb.append("@Entity\n") sb.append("@Table(name = \"").append(table.name).append("\")\n") sb.append("public class ").append(className) if (ENABLE_SERIALIZABLE) { sb.append(" implements Serializable") } sb.append(" {\n\n") if (ENABLE_SERIALIZABLE) { sb.append(" private static final long serialVersionUID = 1L;\n\n") } sb.append(fields) sb.append("}\n") return sb.toString() } def isPrimaryKey(DasTable table, DasColumn col) { try { def pks = DasUtil.getPrimaryKeys(table) if (pks != null && pks.size() > 0) { return pks.any { pk -> pk.name == col.name } } } catch (Exception e) { // 部分 IDEA 版本没有 getPrimaryKeys,退化为列名判断 } return col.name.equalsIgnoreCase("id") } def toCamelCase(String name) { def cleanName = name def lower = name.toLowerCase() if (lower.startsWith("t_")) { cleanName = name.substring(2) } else if (lower.startsWith("tb_")) { cleanName = name.substring(3) } def parts = cleanName.split("_") def result = parts[0].toLowerCase() for (int i = 1; i < parts.length; i++) { result += parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1).toLowerCase() } return result } def toPascalCase(String name) { def camel = toCamelCase(name) return camel.substring(0, 1).toUpperCase() + camel.substring(1) } def mapType(DataType dataType) { def typeName = dataType.name?.toLowerCase() switch (typeName) { case ["int", "int2", "int4"]: return "Integer" case ["int8", "bigint", "bigserial"]: return "Long" case ["varchar", "char", "bpchar", "text", "citext"]: return "String" case ["numeric", "decimal"]: return "BigDecimal" case ["float4", "real"]: return "Float" case ["float8", "double precision"]: return "Double" case ["bool", "boolean"]: return "Boolean" case ["date"]: return "LocalDate" case ["timestamp", "timestamptz", "datetime"]: return "LocalDateTime" case ["time", "timetz"]: return "LocalTime" case ["bytea"]: return "byte[]" case ["uuid"]: return "UUID" default: return "String" } }这个脚本几个值得注意的设计点:
- 文件写入统一用
UTF-8是 Groovy 默认行为,IDEA 底层的File.write会正确处理中文字符,因此注释出现中文不会乱码。 - import 集合用的是
LinkedHashSet,这样能保证 import 顺序稳定,不会出现每次生成结果顺序都不一样的情况。 - 脚本顶部特意放了
ID_STRATEGY配置项,适配不同项目主键策略。 isPrimaryKey方法里做了兼容处理,优先读取数据库主键元数据,读不到就按列名id兜底。实际项目里大部分业务表主键确实就叫id,兜底策略能解决很多兼容问题。
4.2 实际生成的效果预览
假设数据库有一张sys_user表,结构如下:
| 列名 | 类型 | 注释 |
|---|---|---|
| id | int8 | 主键 |
| username | varchar(64) | 用户名 |
| password_hash | varchar(128) | 密码哈希 |
| varchar(128) | 邮箱 | |
| status | int2 | 状态:1启用 0停用 |
| created_at | timestamptz | 创建时间 |
| updated_at | timestamptz | 更新时间 |
| version | int4 | 乐观锁版本号 |
脚本生成出来的类大概是这样:
package com.example.jpa.entity; import java.io.Serializable; import java.time.LocalDateTime; import javax.persistence.*; import lombok.Getter; import lombok.Setter; /** * 系统用户表 * @author your-name */ @Getter @Setter @Entity @Table(name = "sys_user") public class SysUser implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; /** * 用户名 */ @Column(name = "username") private String username; /** * 密码哈希 */ @Column(name = "password_hash") private String passwordHash; /** * 邮箱 */ @Column(name = "email") private String email; /** * 状态:1启用 0停用 */ @Column(name = "status") private Integer status; /** * 创建时间 */ @Column(name = "created_at") private LocalDateTime createdAt; /** * 更新时间 */ @Column(name = "updated_at") private LocalDateTime updatedAt; /** * 乐观锁版本号 */ @Column(name = "version") private Integer version; }这个结果和我手工写实体类的习惯已经很接近了。唯一可能还要手工补的是version字段的@Version注解,以及如果需要乐观锁,可以给脚本增加一个约定:当字段名为version时自动加上@Version。这种约定式生成在团队内部非常实用,强烈建议加上。
4.3 生成之后常见的微调事项
就算脚本再完善,生成完也不可能 100% 不需要动。下面几个调整我几乎每次都遇到。
第一个是主键策略。PostgreSQL 里如果表的主键是使用序列维护的,脚本生成的IDENTITY就要手工改成SEQUENCE,同时补上@SequenceGenerator。这步如果嫌麻烦,可以直接在脚本顶部把ID_STRATEGY改成SEQUENCE并填好序列名。
第二个是关联字段。JPA 实体类里的@ManyToOne、@OneToMany关联,我不建议写在自动生成脚本里。原因很简单:数据库外键关系复杂,自动生成关联注解很可能把只读关联和多表查询搞混。我见过有人试过自动生成关联字段,结果一张主表关联十几个子表,实体关系变得又乱又难维护。关联关系自己手写更稳妥。
第三个是通用字段。比如create_time、update_time这种审计字段,如果你项目里集成了 Hibernate Envers 或者审计框架,生成的LocalDateTime字段往往还要手工加上@CreatedDate、@LastModifiedDate这类注解。这些属于业务规则,脚本不建议过度自动化,生成后按团队规范补一下反而更清晰。
5. 实战中的坑与排查实录
5.1 脚本跑不起来:日志往哪里看
第一次写完 Groovy 脚本,运行的时候几乎不可能一次通过。IDEA 执行数据库脚本时,println 输出会打印在控制台的 Script 输出面板里,一般是在 Run 工具窗口下面,信息很全。
最常见的两个报错:
unable to resolve class com.intellij.database.util.DasUtil:这是 IDEA 版本之间 API 的差异问题。早期版本里的包名是com.intellij.database.model.DasUtil,新版本移动到了com.intellij.database.util.DasUtil。出现这个报错时,把 import 改一下就行。FILES变量未定义:脚本必须通过在数据库面板右键选中表来触发,不是随便建一个 Groovy 文件运行。如果右键菜单里看不到你写的脚本,检查一下 Groovy Scripts Directory 是否配置正确,另外脚本文件名不要用中文。
还有一次我把脚本写得很长,运行时 IDEA 提示语法错误,排查半天发现是switch分支里的中文字符全角逗号混进去了。Groovy 对代码风格比较宽容,但对符号异常仍然零容忍,写脚本时最好打开 IDEA 自带的语法提示。
5.2 类型映射不满足业务:jsonb 字段怎么处理
PostgreSQL 的jsonb类型在 IDA 的DataType里返回的是别名jsonb,按照前面映射表会走default返回String。这在大多数情况下没问题,但如果你要执行 JPA 的Specification查询,直接在 JSONB 字段上用String类型的相等条件会很别扭,因为底层 SQL 需要对 jsonb 做?或者->>操作。
更规范的做法是引入 Hibernate 6 的类型注解:
@JdbcTypeCode(SqlTypes.JSON) @Column(name = "extra_attrs", columnDefinition = "jsonb") private String extraAttrs;或者为 JSONB 字段定义一个简单 Json 转换器,在实体类里用@Convert注解标注。这个逻辑如果在生成脚本里做,需要项目里预先定义好转换器类,然后把脚本里 jsonb 分支从返回String改成返回自定义类型并补上对应 import。我建议接手老项目时先看看团队有没有已经封装好的 JSON 类型处理器,再去改脚本映射。
5.3 连接不上数据库或看不到表
脚本虽然和数据库驱动不直接相关,但脚本读取表结构依赖数据源元数据同步。如果数据库面板里刷新失败,脚本就拿不到列。
常见原因:
- PostgreSQL 驱动版本与数据库版本不匹配,连接测试通过但 schema 列表为空。解决方法是去官方仓库下载对应大版本的 JDBC 驱动,在数据源驱动设置里替换。
- 没有勾选正确 schema。一个数据库里有多个 schema 时,默认只加载当前选中 schema 下的表,需要到数据源的高级配置里把业务 schema 加上。
- ID 连接的是只读账号,IDEA 不会因此拒绝读元数据,但某些云数据库实例的只读账号看不到注释信息。这时候生成出来的实体类就会缺 JavaDoc,只能换账号或者手动补注释。
5.4 如何让脚本成为团队资产
脚本写好后,不要只留在本地,我建议把它作为项目的代码生成工具放在仓库里。目录结构这样划分:
tools/ db-codegen/ GenerateJpaEntityForSpringBoot.groovy README.mdREADME 里写明两件事:第一,这个脚本如何安装到 IDEA 的 Groovy Scripts Directory;第二,生成后的代码规范,包括主键策略、字段映射、Lombok 约定。团队新人配置完环境后,少问很多重复问题。
另外,IDEA 每次大版本升级后,Groovy 脚本的 API 可能会有变化,保持脚本在仓库里意味着你可以在升级后快速修改并同步给团队。这个收益在多人协作的场景下特别明显。
最后分享一个我实际操作中的体会:这个脚本前前后后改了三四版,一开始只是加上 Lombok 和注释,后来才逐步加入类型映射、主键策略配置。现在每次新建表,右键一键生成实体类,再花两分钟检查一下关联关系和审计字段,基本就能提交代码。写脚本这件事,前期花几个小时,后面省下的时间远远不止这几个小时。如果你也每天被实体类生成折磨,强烈建议照着文章的思路定制一版自己的脚本,不用追求一步到位,先解决最痛的问题,再慢慢迭代。