Skip to content

代码生成与类型安全 ​

在前两篇中,我们已经完成了:

  • tio-boot + jOOQ 纯配置类整合(DSLContext 注入)
  • 事务管理
  • 批量操作与性能优化

但目前的 SQL 仍然是这种写法:

java
DSL.table("system_admin")
DSL.field("login_name")

这类字符串方式虽然可用,但它的缺点也很明显:

  • 字段名写错只能运行时报错
  • 重构改字段/表名时无法自动修复
  • IDE 无法提供真正可靠的自动补全
  • 大项目里容易“潜伏 bug”

jOOQ 的“完全体”体验,是通过 代码生成(Codegen) 得到:

  • Tables.SYSTEM_ADMIN
  • SYSTEM_ADMIN.LOGIN_NAME
  • SystemAdminRecord

让的 SQL 进入真正的“编译期可校验”状态。

本文将讲清楚如何在 tio-boot 项目中加入 jOOQ Codegen,并完成类型安全升级。


一、代码生成能带来什么 ​

以查询为例,对比非常直观。

1)生成前(字符串) ​

java
dsl.select(DSL.field("id"), DSL.field("login_name"))
   .from(DSL.table("system_admin"))
   .where(DSL.field("login_name").eq(loginName))
   .fetchOne();

问题:表名/字段名完全是字符串,IDE 很难保证正确性。

2)生成后(类型安全) ​

java
import static demo.jooq.gen.Tables.SYSTEM_ADMIN;

dsl.select(SYSTEM_ADMIN.ID, SYSTEM_ADMIN.LOGIN_NAME)
   .from(SYSTEM_ADMIN)
   .where(SYSTEM_ADMIN.LOGIN_NAME.eq(loginName))
   .fetchOne();

收益:

  • 字段不存在会直接编译失败
  • 重构字段名,引用会跟着改
  • 自动补全非常强
  • Record/POJO 映射更自然

二、jOOQ Codegen 的基本思路 ​

jOOQ 代码生成的本质是:

  1. 连接数据库
  2. 读取 schema 元数据(表、字段、类型、约束、枚举等)
  3. 生成 Java 类到指定目录与包名

因此它不是运行时做的事,而是构建时做的事。

常用工作流:

  • 本地开发:执行 mvn -DskipTests generate-sources
  • CI:生成并编译
  • 生成代码通常加入 Git(也可以不加入,取决于团队习惯)

三、在 pom.xml 添加 jOOQ 代码生成插件 ​

当前使用:

  • Java 21
  • jOOQ 3.18.6
  • PostgreSQL

建议在 pom.xml 增加这些属性(可选但推荐统一版本):

xml
<properties>
  <jooq.version>3.18.6</jooq.version>
</properties>

然后在 <build><plugins> 里加入 jooq-codegen-maven 插件。

1)插件配置(PostgreSQL) ​

xml
<build>
  <plugins>
    <plugin>
      <groupId>org.jooq</groupId>
      <artifactId>jooq-codegen-maven</artifactId>
      <version>${jooq.version}</version>

      <executions>
        <execution>
          <id>jooq-codegen</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>generate</goal>
          </goals>
        </execution>
      </executions>

      <configuration>
        <!-- JDBC 连接信息:用于读取数据库元数据 -->
        <jdbc>
          <driver>org.postgresql.Driver</driver>
          <url>jdbc:postgresql://192.168.3.151/defaultdb</url>
          <user>postgres</user>
          <password>1234566</password>
        </jdbc>

        <generator>
          <database>
            <name>org.jooq.meta.postgres.PostgresDatabase</name>

            <!-- 生成哪个 schema 的表 -->
            <inputSchema>public</inputSchema>

            <!-- 可选:只生成匹配的表(正则) -->
            <includes>.*</includes>
            <!-- 可选:排除表 -->
            <!-- <excludes>flyway_schema_history</excludes> -->
          </database>

          <generate>
            <!-- 生成 Record / POJO / DAO 看团队习惯 -->
            <records>true</records>
            <pojos>true</pojos>
            <daos>false</daos>

            <!-- 让 POJO 上生成 setter/getter 更符合常用习惯 -->
            <fluentSetters>false</fluentSetters>
          </generate>

          <target>
            <!-- 生成代码包名(建议独立 gen 包) -->
            <packageName>demo.jooq.gen</packageName>

            <!-- 生成目录(Maven 标准目录) -->
            <directory>${project.build.directory}/generated-sources/jooq</directory>
          </target>
        </generator>
      </configuration>

      <!-- 插件运行依赖:必须带上 jdbc 驱动 -->
      <dependencies>
        <dependency>
          <groupId>org.postgresql</groupId>
          <artifactId>postgresql</artifactId>
          <version>42.5.0</version>>
        </dependency>
      </dependencies>

    </plugin>

    <!-- 让 Maven 自动把 generated-sources 加入编译源(多数 IDE 会识别,但建议显式) -->
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>build-helper-maven-plugin</artifactId>
      <version>3.5.0</version>
      <executions>
        <execution>
          <id>add-generated-sources</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>add-source</goal>
          </goals>
          <configuration>
            <sources>
              <source>${project.build.directory}/generated-sources/jooq</source>
            </sources>
          </configuration>
        </execution>
      </executions>
    </plugin>

  </plugins>
</build>

说明要点:

  • phase=generate-sources:执行 Maven 生成源码阶段
  • target/directory:统一放到 target/generated-sources/jooq
  • packageName:建议固定生成包,避免污染业务包
  • build-helper-maven-plugin:确保编译时能找到生成代码

四、执行生成 ​

在项目根目录执行:

bash
mvn -DskipTests generate-sources

成功后会看到类似结构:

target/generated-sources/jooq/demo/jooq/gen/Tables.java
target/generated-sources/jooq/demo/jooq/gen/tables/SystemAdmin.java
target/generated-sources/jooq/demo/jooq/gen/tables/records/SystemAdminRecord.java
target/generated-sources/jooq/demo/jooq/gen/tables/pojos/SystemAdmin.java

建议 IDE 操作:

  • 把 target/generated-sources/jooq 标记为 Generated Sources Root
  • 避免生成代码被误格式化/误改

跳过jooq生成

bash
mvn clean install -DskipTests -Dgpg.skip -Djooq.codegen.skip=true

五、类型安全升级:DAO 改造示例 ​

之前的 DAO(字符串方式):

java
dsl.select(DSL.field("id"), DSL.field("login_name"))
   .from(DSL.table("system_admin"))
   .where(DSL.field("login_name").eq(loginName))
   .fetchOneInto(SystemAdmin.class);

升级为生成代码后(强类型):

1)使用生成的 Tables ​

java
package demo.jooq.dao;

import org.jooq.DSLContext;

import nexus.io.annotation.Inject;

import demo.jooq.tx.TransactionContext;

import static demo.jooq.gen.Tables.SYSTEM_ADMIN;

// 这里建议返回自己的 DTO 或生成的 POJO,都可以
import demo.jooq.model.SystemAdmin;

public class SystemAdminDao {

  @Inject
  private DSLContext dsl;

  private DSLContext useDsl() {
    DSLContext txDsl = TransactionContext.get();
    return txDsl != null ? txDsl : dsl;
  }

  public SystemAdmin findByLoginName(String loginName) {
    return useDsl()
      .select(SYSTEM_ADMIN.ID, SYSTEM_ADMIN.LOGIN_NAME, SYSTEM_ADMIN.PASSWORD)
      .from(SYSTEM_ADMIN)
      .where(SYSTEM_ADMIN.LOGIN_NAME.eq(loginName))
      .fetchOneInto(SystemAdmin.class);
  }

  public int updatePassword(String loginName, String newPassword) {
    return useDsl()
      .update(SYSTEM_ADMIN)
      .set(SYSTEM_ADMIN.PASSWORD, newPassword)
      .where(SYSTEM_ADMIN.LOGIN_NAME.eq(loginName))
      .execute();
  }
}

这里的关键变化:

  • SYSTEM_ADMIN 是生成的表对象,不会写错
  • SYSTEM_ADMIN.LOGIN_NAME 是生成字段对象,有类型信息
  • eq(loginName) 也能获得更严格的类型推导

六、选择:使用生成的 POJO 还是业务 DTO ​

生成后会拿到:

  • demo.jooq.gen.tables.pojos.SystemAdmin(生成 POJO)
  • demo.jooq.gen.tables.records.SystemAdminRecord(Record)

有两种主流选择:

方案 A:业务 DTO 继续用自己的(更推荐) ​

优点:

  • 业务层结构稳定
  • 不被生成包结构绑定
  • 未来切换生成策略影响小

缺点:

  • 多一层映射(但 jOOQ 的 fetchInto 通常足够)

方案 B:直接使用生成 POJO(开发更快) ​

优点:

  • 少写一个 DTO
  • 映射最直接

缺点:

  • 业务层依赖生成代码包结构
  • 生成策略变化会影响业务代码

通常建议:

  • DAO 层可以用生成 POJO / Record
  • Controller 返回仍然用自己的 DTO(更可控)

七、进阶:强制类型映射(forcedTypes) ​

很多团队会遇到这种需求:

  • jsonb 映射为 String 或 Map
  • timestamp 映射为 LocalDateTime
  • 自定义 enum 类型映射为 Java enum

这就需要 forcedTypes。

示例(把 jsonb 映射为 String):

xml
<database>
  <name>org.jooq.meta.postgres.PostgresDatabase</name>
  <inputSchema>public</inputSchema>

  <forcedTypes>
    <forcedType>
      <name>VARCHAR</name>
      <includeTypes>jsonb</includeTypes>
    </forcedType>
  </forcedTypes>
</database>

后续也可以进一步使用 Converter,把 jsonb 转成 Map<String,Object>(通常需要自定义 converter 类)。


八、生成策略建议 ​

1)生成范围控制 ​

大库建议不要全生成:

  • 只生成特定表:<includes>system_admin|audit_log</includes>
  • 排除历史表/临时表:<excludes>tmp_.*|bak_.*</excludes>

2)生成代码是否提交 Git ​

常见两种流派:

  • 提交:构建更稳定,避免“本地没生成导致编译失败”
  • 不提交:仓库更干净,但 CI 必须保证生成

如果团队没有严格 CI,建议先提交生成代码,后面再演进。


九、总结 ​

加入 jOOQ Codegen 后,的 tio-boot + jOOQ 体系会从“可用”提升为“强类型工程化”:

  • 表/字段编译期校验
  • IDE 补全与重构能力极强
  • 大幅减少运行期 SQL 拼写错误
  • 事务、批量、性能优化都能继续复用原有设计

MIT Licensed