Skip to content

tio-boot + jOOQ 事务管理 ​

本文将基于已经完成的 tio-boot + jOOQ(纯配置类方式、AOP 注入 DSLContext) 的整合方案,继续讲清楚在该架构下如何实现 事务管理。

目标是做到:

  • 以 纯 Java 方式 管理事务
  • 不依赖 Spring,不引入额外框架
  • 支持手动事务、AOP 事务两种写法
  • 明确事务边界,避免连接泄漏与隐式提交
  • 与 tio-boot 生命周期与 AOP 注入自然融合

一、jOOQ 事务的本质 ​

jOOQ 本身不直接“持有连接池”,它的事务能力依赖两点:

  1. ConnectionProvider:决定从哪里获取连接、如何释放
  2. TransactionProvider:决定如何开启、提交、回滚事务

当前的写法:

java
DSLContext dslContext = DSL.using(dataSource, SQLDialect.POSTGRES);

这会让 jOOQ 使用默认的连接获取方式(底层走 DataSource),但如果希望:

  • 同一事务内多次 DAO 调用复用同一连接
  • 可控地 commit/rollback
  • 在 service 层统一控制事务边界

就需要把“事务”从 DAO 的一次查询,提升到“业务方法级别”。


二、事务管理的两种常见方式 ​

在 tio-boot + jOOQ 中推荐两种方式:

方式 A:手动事务(最直观) ​

适合:

  • 事务逻辑少
  • 希望完全显式控制
  • 学习和定位问题更容易

方式 B:AOP 事务(最省代码) ​

适合:

  • 大量业务方法需要事务
  • 希望统一规范事务边界
  • 类似 Spring 的 @Transactional 体验

本文两种都给出完整写法。


三、准备:抽象一个 TransactionManager ​

3.1 目标 ​

我们希望业务层这样写:

java
txManager.tx(() -> {
  daoA.xxx();
  daoB.yyy();
});

不关心 Connection 的创建、关闭、提交、回滚。

3.2 TransactionManager 实现 ​

java
package demo.jooq.tx;

import java.sql.Connection;

import javax.sql.DataSource;

import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TransactionManager {

  private final DataSource dataSource;
  private final SQLDialect dialect;

  public TransactionManager(DataSource dataSource, SQLDialect dialect) {
    this.dataSource = dataSource;
    this.dialect = dialect;
  }

  /**
   * 执行无返回值事务
   */
  public void tx(TxRunnable runnable) {
    tx(() -> {
      runnable.run();
      return null;
    });
  }

  /**
   * 执行有返回值事务
   */
  public <T> T tx(TxCallable<T> callable) {
    Connection conn = null;
    try {
      conn = dataSource.getConnection();
      conn.setAutoCommit(false);

      DSLContext txDsl = DSL.using(conn, dialect);
      TransactionContext.set(txDsl);

      T result = callable.call();

      conn.commit();
      return result;
    } catch (Exception e) {
      if (conn != null) {
        try {
          conn.rollback();
        } catch (Exception ignore) {
        }
      }
      log.error("transaction rollback", e);
      throw new RuntimeException(e);
    } finally {
      TransactionContext.clear();
      if (conn != null) {
        try {
          conn.setAutoCommit(true);
          conn.close();
        } catch (Exception ignore) {
        }
      }
    }
  }

  @FunctionalInterface
  public interface TxRunnable {
    void run() throws Exception;
  }

  @FunctionalInterface
  public interface TxCallable<T> {
    T call() throws Exception;
  }
}

四、关键点:让 DAO 在事务内拿到同一个 DSLContext ​

如果 DAO 里一直用 @Inject DSLContext dsl;,那么它用的是全局单例 DSLContext,这会导致:

  • DAO 每次执行 SQL 都可能拿到不同连接
  • 事务内不能保证连接一致
  • 出现“看似开启事务但实际提交了”的错觉

解决方案:引入一个 TransactionContext(ThreadLocal)。


五、TransactionContext 实现 ​

java
package demo.jooq.tx;

import org.jooq.DSLContext;

public class TransactionContext {

  private static final ThreadLocal<DSLContext> CTX = new ThreadLocal<>();

  public static void set(DSLContext dsl) {
    CTX.set(dsl);
  }

  public static DSLContext get() {
    return CTX.get();
  }

  public static void clear() {
    CTX.remove();
  }
}

六、改造 DAO:优先使用事务 DSLContext ​

把 DAO 改成:

  • 如果当前线程存在事务 DSLContext,就用它
  • 否则退回使用全局注入的 DSLContext

SystemAdminDao 改造版 ​

java
package demo.jooq.dao;

import org.jooq.DSLContext;
import org.jooq.impl.DSL;

import nexus.io.annotation.Inject;

import demo.jooq.model.SystemAdmin;
import demo.jooq.tx.TransactionContext;

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(DSL.field("id"), DSL.field("login_name"), DSL.field("password"))
      .from(DSL.table("system_admin"))
      .where(DSL.field("login_name").eq(loginName))
      .fetchOneInto(SystemAdmin.class);
  }

  public int updatePassword(String loginName, String newPassword) {
    return useDsl()
      .update(DSL.table("system_admin"))
      .set(DSL.field("password"), newPassword)
      .where(DSL.field("login_name").eq(loginName))
      .execute();
  }
}

七、在配置类中注册 DataSource 与 TransactionManager ​

之前配置类里只注册了 DSLContext,事务需要 DataSource,因此建议:

  • 把 DataSource 也注册到 AOP
  • 再注册 TransactionManager

JooqConfig(带事务增强版) ​

java
package demo.jooq.config;

import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;

import com.alibaba.druid.pool.DruidDataSource;
import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.constants.ServerConfigKeys;
import nexus.io.hook.HookCan;
import nexus.io.jfinal.aop.AopManager;
import nexus.io.tio.utils.environment.EnvUtils;

import demo.jooq.tx.TransactionManager;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@AConfiguration
public class JooqConfig {

  @Initialization
  public void initJooq() {

    String jdbcUrl = EnvUtils.get(ServerConfigKeys.JDBC_URL);
    String username = EnvUtils.getStr(ServerConfigKeys.JDBC_USER);
    String password = EnvUtils.getStr(ServerConfigKeys.JDBC_PSWD);

    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(jdbcUrl);
    dataSource.setUsername(username);
    dataSource.setPassword(password);

    DSLContext dslContext = DSL.using(dataSource, SQLDialect.POSTGRES);

    // 注册核心对象
    AopManager.me().addSingletonObject(DruidDataSource.class, dataSource);
    AopManager.me().addSingletonObject(DSLContext.class, dslContext);

    // 注册事务管理器
    TransactionManager txManager = new TransactionManager(dataSource, SQLDialect.POSTGRES);
    AopManager.me().addSingletonObject(TransactionManager.class, txManager);

    log.info("jOOQ + TransactionManager initialized");

    HookCan.me().addDestroyMethod(() -> {
      dataSource.close();
      log.info("DruidDataSource closed");
    });
  }
}

八、Service 层示例:事务提交与回滚 ​

8.1 一个典型业务:修改密码并记录日志(示例) ​

假设有两个 DAO:

  • SystemAdminDao.updatePassword
  • AuditLogDao.insert

业务要求:两者要么都成功,要么都失败。

java
package demo.jooq.service;

import nexus.io.annotation.Inject;

import demo.jooq.dao.SystemAdminDao;
import demo.jooq.tx.TransactionManager;

public class SystemAdminService {

  @Inject
  private SystemAdminDao systemAdminDao;

  @Inject
  private TransactionManager txManager;

  public void changePassword(String loginName, String newPassword) {
    txManager.tx(() -> {
      systemAdminDao.updatePassword(loginName, newPassword);

      // 模拟异常:测试回滚
      // if (true) throw new RuntimeException("mock error");
    });
  }
}

只要事务内部抛异常,就会 rollback。


九、Controller 调用 Service ​

java
package demo.jooq.controller;

import com.jfinal.kit.Kv;
import nexus.io.annotation.Inject;
import nexus.io.annotation.RequestPath;

import demo.jooq.service.SystemAdminService;

@RequestPath("/systemAdmin")
public class SystemAdminController {

  @Inject
  private SystemAdminService service;

  public Kv changePassword() {
    service.changePassword("litong", "11111111");
    return Kv.by("ok", true);
  }
}

十、方式 B:AOP 事务(让业务代码更干净) ​

如果的项目事务很多,建议实现一个注解:

  • @ATransactional
  • AOP 拦截器自动包裹 txManager.tx()

10.1 注解 ​

java
package demo.jooq.tx;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ATransactional {
}

10.2 拦截器(伪代码风格,按当前 AOP 体系调整) ​

不同版本的 jfinal-aop 拦截器接口可能略有差异,下面是表达核心思想的实现方式:

java
package demo.jooq.tx;

import nexus.io.jfinal.aop.Aop;
import nexus.io.jfinal.aop.AopInterceptor;
import nexus.io.jfinal.aop.AopInvocation;

public class TransactionInterceptor implements AopInterceptor {

  @Override
  public void intercept(AopInvocation inv) {
    TransactionManager txManager = Aop.get(TransactionManager.class);
    txManager.tx(() -> {
      inv.invoke();
    });
  }
}

10.3 绑定拦截器 ​

把标注了 @ATransactional 的方法自动应用拦截器。

具体绑定方式取决于项目里 jfinal-aop 的配置方式,常见做法是:

  • 在初始化阶段扫描注解并绑定
  • 或在 AOP 框架支持的配置点进行注册

可以把它写成一个独立配置类 TransactionAopConfig,在 @Initialization 中完成注册。

java
package demo.jooq.config;

import nexus.io.context.BootConfiguration;
import nexus.io.jfinal.aop.AopInterceptorManager;

import demo.jooq.tx.ATransactional;
import demo.jooq.tx.TransactionInterceptor;

public class JooqBootConfig implements BootConfiguration{

  @Override
  public void config() throws Exception {
    AopInterceptorManager.me().registerInterceptor(ATransactional.class, TransactionInterceptor.class);
  }
}
java
  @ATransactional
  public void changePassword2(String loginName, String newPassword) {
    systemAdminDao.updatePassword(loginName, newPassword);
  }

十一、事务管理的常见坑 ​

1. DAO 内部不要自己开启事务 ​

事务边界应该在 service 层统一控制,否则会出现嵌套事务语义混乱。

2. 必须确保事务内复用同一连接 ​

所以才需要 TransactionContext(ThreadLocal)。

3. 不要把 Connection 暴露给业务代码 ​

暴露 Connection 会导致资源泄漏、commit/rollback 混乱、可维护性下降。


十二、整合Quarkus完整配置 ​

java
package demo.jooq.config;

import java.time.Duration;

import javax.sql.DataSource;

import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;

import nexus.io.constants.ServerConfigKeys;
import nexus.io.context.BootConfiguration;
import nexus.io.hook.HookCan;
import nexus.io.jfinal.aop.Aop;
import nexus.io.jfinal.aop.AopManager;
import nexus.io.jfinal.aop.AopInterceptorManager;
import nexus.io.tio.utils.environment.EnvUtils;

import io.agroal.api.AgroalDataSource;
import io.agroal.api.configuration.supplier.AgroalConnectionFactoryConfigurationSupplier;
import io.agroal.api.configuration.supplier.AgroalConnectionPoolConfigurationSupplier;
import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class QaTestConfig implements BootConfiguration {

  @Override
  public void config() throws Exception {
    //需要提前添加
    AopInterceptorManager.me().registerInterceptor(Transactional.class, TransactionInterceptor.class);

    String jdbcUrl = EnvUtils.get(ServerConfigKeys.JDBC_URL);
    String username = EnvUtils.getStr(ServerConfigKeys.JDBC_USER);
    String password = EnvUtils.getStr(ServerConfigKeys.JDBC_PSWD);

    // 1) 连接工厂配置(JDBC URL / 驱动 / 账号密码)
    AgroalConnectionFactoryConfigurationSupplier connectionFactory = new AgroalConnectionFactoryConfigurationSupplier()
        // PostgreSQL 驱动类(按你的数据库替换)
        .connectionProviderClass(org.postgresql.Driver.class).jdbcUrl(jdbcUrl)
        .principal(new io.agroal.api.security.NamePrincipal(username))
        .credential(new io.agroal.api.security.SimplePassword(password));

    // 2) 连接池配置(按需调优)
    AgroalConnectionPoolConfigurationSupplier poolConfig = new AgroalConnectionPoolConfigurationSupplier()
        .connectionFactoryConfiguration(connectionFactory).minSize(0).maxSize(20)
        // 获取连接超时
        .acquisitionTimeout(Duration.ofSeconds(30))
        // 可选:连接最大生命周期
        .maxLifetime(Duration.ofMinutes(30));

    // 3) DataSource 配置 + 创建数据源
    AgroalDataSourceConfigurationSupplier dsConfig = new AgroalDataSourceConfigurationSupplier()
        .connectionPoolConfiguration(poolConfig)
        // 可选:开启 metrics(你如果不用就删掉)
        .metricsEnabled(true);

    AgroalDataSource agroalDataSource = AgroalDataSource.from(dsConfig);

    // 4) jOOQ DSLContext
    DSLContext dslContext = DSL.using((DataSource) agroalDataSource, SQLDialect.POSTGRES);

    // 5) 注册核心对象
    AopManager.me().addSingletonObject(AgroalDataSource.class, agroalDataSource);
    AopManager.me().addSingletonObject(DSLContext.class, dslContext);

    // 6) 注册事务管理器(你的 TransactionManager 如果只依赖 DataSource,直接传 agroalDataSource 即可)
    TransactionManager txManager = new TransactionManager(agroalDataSource, SQLDialect.POSTGRES);
    AopManager.me().addSingletonObject(TransactionManager.class, txManager);
    Aop.addFetchBeanAnnotation(Inject.class);

    log.info("jOOQ + TransactionManager initialized (Agroal)");

    // 7) 优雅关闭
    HookCan.me().addDestroyMethod(() -> {
      try {
        agroalDataSource.close();
        log.info("AgroalDataSource closed");
      } catch (Exception e) {
        log.warn("Failed to close AgroalDataSource", e);
      }
    });
  }
}

十三、总结 ​

本文给出了 tio-boot + jOOQ 的事务管理完整方案,特点是:

  • 纯 Java 配置
  • 不依赖 Spring
  • 事务边界清晰(service 层)
  • ThreadLocal 保证事务连接一致
  • 同时支持手动事务与 AOP 事务风格

MIT Licensed