jOOQ 是基于 JDBC 之上的一个抽象层,提供了多种多样的模型来与关系型数据库进行互操作;其使用与 mybatisHibernate ORM 不同的思路来实现 对象关系映射 ORM

本篇主要介绍 jOOQ 的事务支持。

🔗Common

本文使用了 代码生成 里面的基于 PostgreSQL 实例驱动生成的代码,代码结构如下:

├── tables
│   ├── daos
│   │   ├── AuthorDao.java
│   │   └── BookDao.java
│   ├── interfaces
│   │   ├── IAuthor.java
│   │   └── IBook.java
│   ├── pojos
│   │   ├── Author.java
│   │   └── Book.java
│   ├── records
│   │   ├── AuthorRecord.java
│   │   └── BookRecord.java
│   ├── Author.java
│   └── Book.java
├── DefaultCatalog.java
├── Indexes.java
├── Keys.java
├── Public.java
├── Sequences.java
└── Tables.java

所有的用例都基于 JUnit 5 框架,测试用的数据库提供方为 H2 ;测试代码中使用了参数化测试功能。

预准备阶段需要准备数据库:

private DSLContext dsl;

@BeforeEach
void setUp(@TempDir final Path dir) throws Exception {
	final String url = "jdbc:h2:" + dir.toString() + "/test.db";
	final Connection connection = DriverManager.getConnection(url, "sa", "");

	dsl = DSL.using(connection, SQLDialect.H2);

	// Create table
	dsl.createTableIfNotExists(Tables.AUTHOR)
		.columns(Tables.AUTHOR.ID, Tables.AUTHOR.FIRST_NAME, Tables.AUTHOR.LAST_NAME,
			Tables.AUTHOR.DATE_OF_BIRTH, Tables.AUTHOR.YEAR_OF_BIRTH)
		.constraint(DSL.primaryKey(Tables.AUTHOR.ID))
		.execute();
}

@AfterEach
void tearDown() {
	dsl.close();
}

同时,为了简化测试代码,新建了 2 个接口类用于辅助模拟抛出不同异常的情况。

interface ThrowingConsumer {
	void run(final AuthorRecord r) throws Exception;
}

interface ThrowingFunction<T> {
	T call(final AuthorRecord r) throws Exception;
}

🔗TransactionalRunnable

可以使用 org.jooq.TransactionalRunnable 接口封装需要在事务中执行的代码;封装好的代码需要提交给 org.jooq.DSLContext#transaction(org.jooq.TransactionalRunnable) 方法执行。

示例代码如下:

@ParameterizedTest
@MethodSource("runnables")
void test_transactionRun(final ThrowingConsumer runnable) {
	final int created = dsl.createTableIfNotExists(Tables.AUTHOR)
		.columns(Tables.AUTHOR.ID, Tables.AUTHOR.FIRST_NAME, Tables.AUTHOR.LAST_NAME,
			Tables.AUTHOR.DATE_OF_BIRTH, Tables.AUTHOR.YEAR_OF_BIRTH)
		.constraint(DSL.primaryKey(Tables.AUTHOR.ID))
		.execute();
	Assertions.assertThat(created).isEqualTo(0);

	try {
		dsl.transaction(c -> {
			final DSLContext inner = DSL.using(c);

			final AuthorRecord inserted = inner.insertInto(Tables.AUTHOR)
				.columns(Tables.AUTHOR.FIRST_NAME, Tables.AUTHOR.LAST_NAME)
				.values("Elvis", "Wang")
				.returning(Tables.AUTHOR.ID)
				.fetchOne();

			// assertj
			Assertions.assertThat(inserted).isNotNull();

			runnable.run(inserted);

			final int n = inner.update(Tables.AUTHOR)
				.set(Tables.AUTHOR.FIRST_NAME, "James")
				.set(Tables.AUTHOR.LAST_NAME, "Zhang")
				.where(Tables.AUTHOR.ID.eq(inserted.getId()))
				.execute();

			// assertj
			Assertions.assertThat(n).isEqualTo(1);
		});
	} catch (DataAccessException ex) {
		ex.printStackTrace();
		System.out.println("Failed due to checked exception thrown in transaction");
	} catch (RuntimeException ex) {
		ex.printStackTrace();
		System.out.println("Failed due to unchecked exception thrown in transaction");
	}

	dump(Tables.AUTHOR.getName());
}

在封装好的事务代码中,如果正常运行无异常,则事务被顺利提交;如果抛出受检异常 (Checked Exception) 则事务回滚,且 org.jooq.DSLContext#transaction(org.jooq.TransactionalRunnable) 方法抛出 org.jooq.exception.DataAccessException;如果抛出非受检异常 (Unchecked Exception) 则事务回滚,且该异常被 org.jooq.DSLContext#transaction(org.jooq.TransactionalRunnable) 方法原样抛出。

如果在事务代码中需要使用 org.jooq.DSLContext 实例执行 jOOQ 代码,需要重新构造一个实例而不能使用外部的实例 (示例代码中使用了 inner 而不是 dsl) 。

测试使用的参数化数据从静态方法 runnables 的返回值中获取,如下:

private static Long throwsNothing(final AuthorRecord inserted) throws Exception {
	System.out.println("Throws Nothing when processing " + inserted.getId());
	return inserted.getId();
}

private static Long throwsCheckedException(final AuthorRecord inserted) throws Exception {
	System.out.println("Throws CheckedException when processing " + inserted.getId());
	throw new Exception("Failed in processing " + inserted.getId());
}

private static Long throwsUncheckedException(final AuthorRecord inserted) throws Exception {
	System.out.println("Throws UncheckedException when processing " + inserted.getId());
	throw new RuntimeException("Failed in processing " + inserted.getId());
}

static Stream<Arguments> runnables() {
	return Stream.<ThrowingConsumer>of(
		TransactionIT::throwsNothing,
		TransactionIT::throwsCheckedException,
		TransactionIT::throwsUncheckedException
	).map(Arguments::of);
}

🔗TransactionalCallable

如果封装的事务代码需要返回值,可以使用 org.jooq.TransactionalCallable<T> 接口封装需要在事务中执行的代码;封装好的代码需要提交给 org.jooq.DSLContext#transactionResult(org.jooq.TransactionalCallable<T>) 方法执行。

示例代码如下:

@ParameterizedTest
@MethodSource("callables")
void test_transactionCall(final ThrowingFunction<Long> callable) {
	try {
		final Long result = dsl.transactionResult(c -> {
			final DSLContext inner = DSL.using(c);

			final AuthorRecord inserted = inner.insertInto(Tables.AUTHOR)
				.columns(Tables.AUTHOR.FIRST_NAME, Tables.AUTHOR.LAST_NAME)
				.values("Elvis", "Wang")
				.returning(Tables.AUTHOR.ID)
				.fetchOne();

			// assertj
			Assertions.assertThat(inserted).isNotNull();

			final Long val = callable.call(inserted);

			// assertj
			Assertions.assertThat(val).isNotNull();

			final int n = inner.update(Tables.AUTHOR)
				.set(Tables.AUTHOR.FIRST_NAME, "James")
				.set(Tables.AUTHOR.LAST_NAME, "Zhang")
				.where(Tables.AUTHOR.ID.eq(inserted.getId()))
				.execute();

			// assertj
			Assertions.assertThat(n).isEqualTo(1);

			return val;
		});

		// assertj
		Assertions.assertThat(result).isNotNull();
	} catch (DataAccessException ex) {
		ex.printStackTrace();
		System.out.println("Failed due to checked exception thrown in transaction");
	} catch (RuntimeException ex) {
		ex.printStackTrace();
		System.out.println("Failed due to unchecked exception thrown in transaction");
	}

	dump(Tables.AUTHOR.getName());
}

在封装好的事务代码中,如果正常运行无异常,则事务被顺利提交;如果抛出受检异常 (Checked Exception) 则事务回滚,且 org.jooq.DSLContext#transactionResult(org.jooq.TransactionalCallable<T>) 方法抛出 org.jooq.exception.DataAccessException;如果抛出非受检异常 (Unchecked Exception) 则事务回滚,且该异常被 org.jooq.DSLContext#transactionResult(org.jooq.TransactionalCallable<T>) 方法原样抛出。

如果在事务代码中需要使用 org.jooq.DSLContext 实例执行 jOOQ 代码,需要重新构造一个实例而不能使用外部的实例 (示例代码中使用了 inner 而不是 dsl) 。

测试使用的参数化数据从静态方法 runnables 的返回值中获取,如下:

private static Long throwsNothing(final AuthorRecord inserted) throws Exception {
	System.out.println("Throws Nothing when processing " + inserted.getId());
	return inserted.getId();
}

private static Long throwsCheckedException(final AuthorRecord inserted) throws Exception {
	System.out.println("Throws CheckedException when processing " + inserted.getId());
	throw new Exception("Failed in processing " + inserted.getId());
}

private static Long throwsUncheckedException(final AuthorRecord inserted) throws Exception {
	System.out.println("Throws UncheckedException when processing " + inserted.getId());
	throw new RuntimeException("Failed in processing " + inserted.getId());
}

static Stream<Arguments> callables() {
	return Stream.<ThrowingFunction<Long>>of(
		TransactionIT::throwsNothing,
		TransactionIT::throwsCheckedException,
		TransactionIT::throwsUncheckedException
	).map(Arguments::of);
}

🔗TransactionProvider

jOOQ 通过 org.jooq.TransactionProvider 接口提供了事务实现的自定义支持:

public interface TransactionProvider {
    void begin(TransactionContext ctx) throws DataAccessException;

    void commit(TransactionContext ctx) throws DataAccessException;

    void rollback(TransactionContext ctx) throws DataAccessException;
}

自定义的 TransactionProvider 实现可以在构造 org.jooq.DSLContext 的时候指定;如果不指定的话,默认使用基于 java.sql.Savepoint 的实现。

官方文档 提供了一个使用 SpringDataSourceTransactionManager 自定义实现 TransactionProvider 的例子。


完整的示例代码可以参见 jOOQ Usecases


jOOQ 提供了非声明式的事务支持,详见 官方文档

以上。