Extension for transaction-outbox-core which integrates with jOOQ for transaction management.
Like Transaction Outbox, jOOQ is intended to play nicely with any other transaction management approach, but provides its own as an option. If you are already using jOOQ's TransactionProvider
via DSLContext.transaction(...)
throughout your application, you can continue to do so with this extension.
jOOQ gives you the option to either use thread-local transaction management or explicitly pass a contextual DSLContext
or Configuration
down your stack. You can do the same thing with TransactionOutbox
.
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-jooq</artifactId>
<version>5.5.447</version>
</dependency>
implementation 'com.gruelbox:transactionoutbox-jooq:5.5.447'
See transactionoutbox-core for more information.
First, configure jOOQ to use thread-local transaction management:
var jooqConfig = new DefaultConfiguration();
var connectionProvider = new DataSourceConnectionProvider(dataSource);
jooqConfig.setConnectionProvider(connectionProvider);
jooqConfig.setSQLDialect(SQLDialect.H2);
jooqConfig.setTransactionProvider(new ThreadLocalTransactionProvider(connectionProvider, true));
Now connect JooqTransactionListener
, which is the bridge between jOOQ and TransactionOutbox
, and create the DSLContext
:
var listener = JooqTransactionManager.createListener();
jooqConfig.set(listener);
var dsl = DSL.using(jooqConfig);
Finally create the TransactionOutbox
:
var outbox = TransactionOutbox.builder()
.transactionManager(JooqTransactionManager.create(dsl, listener))
.persistor(Persistor.forDialect(Dialect.MY_SQL_8))
.build();
}
You can now use jOOQ and Transaction Outbox together, assuming thread-bound transactions.
dsl.transaction(() -> {
customerDao.save(new Customer(1L, "Martin", "Carthy"));
customerDao.save(new Customer(2L, "Dave", "Pegg"));
outbox.schedule(MyClass.class).publishCustomerCreatedEvent(1L);
outbox.schedule(MyClass.class).publishCustomerCreatedEvent(2L);
});
If you prefer not to use thread-local transactions, you are already taking on the burden of passing jOOQ Configuration
s or DSLContext
s down your stack. This is supported with TransactionOutbox
, but requires a little explanation.
Without the need to synchronise the thread context, setup is a bit easier:
// Create the DSLContext and connect the listener
var dsl = DSL.using(dataSource, SQLDialect.H2);
dsl.configuration().set(JooqTransactionManager.createListener());
// Create the outbox
var outbox = TransactionOutbox.builder()
.transactionManager(JooqTransactionManager.create(dsl))
.persistor(Persistor.forDialect(Dialect.MY_SQL_8))
.build();
The call pattern in the thread-local context example above will now not work:
outbox.schedule(MyClass.class).publishCustomerCreatedEvent(1L);
TransactionOutbox
needs the currently active transaction context to write the database record. To do so, you need to change the scheduled method itself to receive a Configuration
:
void publishCustomerCreatedEvent(long id, Configuration cfg2) {
cfg.dsl().insertInto(...)...
}
Then call accordingly:
dsl.transaction(cfg1 -> {
new CustomerDao(cfg1).save(new Customer(1L, "Martin", "Carthy"));
new CustomerDao(cfg1).save(new Customer(2L, "Dave", "Pegg"));
outbox.schedule(MyClass.class).publishCustomerCreatedEvent(1L, cfg1);
outbox.schedule(MyClass.class).publishCustomerCreatedEvent(2L, cfg1);
});
In the example above, cfg1
is the transaction context in which the request is written to the database, and cfg2
is the context in which it is executed, which will be a different transaction at some later time. cfg1
is stripped from the request before writing it to the database and replaced with cfg2
at run time.
The reason for passing the Configuration
in the scheduled method call itself (rather than the schedule()
method call) is twofold:
- It is very common for tasks to need access to the transaction context at the time they are run in order to participate in that transaction. That way, if any part of the outbox task is rolled back, any work we do inside it is also rolled back.
- If the method were not scheduled by
TransactionOutbox
, but instead called directly, it would need theConfiguration
passed to it anyway. By working this way we ensure that the API for calling directly or scheduled is the same, and therefore the two implementations are interchangeable.