-
Notifications
You must be signed in to change notification settings - Fork 40.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve the jOOQ transaction experience #24049
Comments
My first thoughts are that I'd need to learn more about jOOQ's transaction support to form an opinion. @michael-simons as someone who knows Spring and jOOQ pretty well, your thoughts on this would be more than welcome if you have the time. |
I can only answer from Neo4j's perspective: We decided to provide both imperative and reactive, native Spring Transaction managers, that opt all in. I'm advocating internally for exact that approach. |
How does bailing out work in your case? Is that something that has to be done explicitly via extra configuration? Or is it automatic, once they use your own native transaction APIs? |
If someone goes down into the driver layer - same level as pure JDBC connection - it’s the driver’s transaction that is leading.
A potentially outer one coming from the application container won’t change a thing.
In short: Yes to the „or"
… Am 05.11.2020 um 21:52 schrieb Lukas Eder ***@***.***>:
If people don't like it, they can bail out and use our driver transaction (imperative, async or reactive).
How does bailing out work in your case? Is that something that has to be done explicitly via extra configuration? Or is it automatic, once they use your own native transaction APIs?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub <#24049 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAEAQL3VWDZLUI2J4VKZWHLSOMGAPANCNFSM4TLGBZMA>.
|
Oh interesting, so you start a new one despite there possibly already being one from the container. I kinda tend to think that whoever's first, wins, and the other "party" needs to make sure their transaction is properly nested, unless a new one is requested explicitly... That way, users can nest A in B or B in A, irrespective of what library/framework created A and/or B |
What does a JDBC Connection do when a user grabs one from their driver and
uses it inside the container connection, not knowing that the data source
is managed?
Lukas Eder <notifications@github.com> schrieb am Do. 5. Nov. 2020 um 22:06:
… Oh interesting, so you start a new one despite there possibly already
being one from the container. I kinda tend to think that whoever's first,
wins, and the other "party" needs to make sure their transaction is
properly nested, unless a new one is requested explicitly... That way,
users can nest A in B or B in A, irrespective of what library/framework
created A and/or B
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#24049 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAEAQL64YYB66YBNBQMMXCLSOMHT5ANCNFSM4TLGBZMA>
.
|
Sure, there's always a way to bypass things. But the Spring Boot jOOQ starter configures jOOQ with a managed My question here is whether jOOQ needs to be fixed, or the |
Would this not make this significantly more complicated? Assuming I've understood things correctly, won't it result in either Spring's As far as I can tell, the crux of the problem is jOOQ's async transaction support and the lack of good mapping to Spring's imperative transaction support which is ThreadLocal-based. The MCVE doesn't seem to gain anything from adapting jOOQ's transactions to Spring's Rather than trying to allow users to mix and match the two APIs (which will be complex and may not even be possible), I wonder if we'd be better providing a configuration property that allows uses to specify whether they want to use Spring's transaction management or jOOQ's transaction management. Basically make it easier to opt out of (or into if we flip the default) |
Yes, absolutely, for us maintainers. But not necessarily for the users.
Depending on who starts things, Spring or jOOQ would manage the current transaction, and the "other side" would have to pick it up. Looking at it more closely, jOOQ would actually not start the current transaction itself, but the
That, and that jOOQ's sync transaction APIs can make use of it, as clients of Spring's
Well, if there were any plans of offering "imperative" (or rather "programmatic") transaction support that is not ThreadLocal based in Spring, those could be used by
Yes
My recommendation is to generally not mix things, but people will inevitably try it. Of course, we could also change course here, and disallow mixing things per documentation and implementation, but that would probably break quite a few applications out there, at least in the sync case. A possibility would be to log a warning, about mixing not being recommended. In the async case, jOOQ could just check the
That would be great in addition to what I'm suggesting (and the above thrown exception). Personally, I wouldn't flip the default. If someone uses jOOQ with Spring (Boot), they're very likely to want to use Spring for transaction management (mostly via But it will still mean that people will first have to understand all of this, and make an informed decision, possibly when evaluating things for the first time. Until that decision, things may simply seem not to work. If users are not jOOQ and/or Spring (Boot) power users, they might already be deep down in some rabbit hole, finding it hard to recover from the "mistake" of mixing the two APIs, at least, that's how I perceive it from my support case experience. |
Considering the resources that we have available and the varying costs of some different approaches to fixing this, we think that our best option is to add a configuration property that allows the auto-configuration of |
Makes sense. On our side, we could use reflection to detect whether the |
Unfortunately we have this use-case in one app. Parts of the SpringBoot app has been implemented with JPA, and we are thinking of moving away from that. Meanwhile I have written a helper class similar to //
import org.jooq.ConnectionProvider;
import org.jooq.TransactionContext;
import org.jooq.TransactionProvider;
import org.jooq.impl.ThreadLocalTransactionProvider;
import org.jooq.tools.JooqLogger;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import static org.springframework.transaction.TransactionDefinition.ISOLATION_DEFAULT;
import static org.springframework.transaction.TransactionDefinition.PROPAGATION_NESTED;
public class JooqPlatformTransactionProvider extends ThreadLocalTransactionProvider implements TransactionProvider {
private static final JooqLogger LOGGER = JooqLogger.getLogger(JooqPlatformTransactionProvider.class);
private PlatformTransactionManager txMgr;
private int propagationBehavior;
private int isolationLevel;
public JooqPlatformTransactionProvider(ConnectionProvider provider, PlatformTransactionManager txMgr) {
super(provider);
this.txMgr = txMgr;
this.propagationBehavior = PROPAGATION_NESTED;
this.isolationLevel = ISOLATION_DEFAULT;
}
public JooqPlatformTransactionProvider(ConnectionProvider provider, PlatformTransactionManager txMgr, int propagationBehavior, int isolationLevel) {
super(provider);
this.txMgr = txMgr;
this.propagationBehavior = propagationBehavior;
this.isolationLevel = isolationLevel;
}
@Override
public void begin(TransactionContext ctx) {
LOGGER.debug("Begin transaction");
// This TransactionProvider behaves like jOOQ's DefaultTransactionProvider,
// which supports nested transactions using Savepoints
DefaultTransactionDefinition def = new DefaultTransactionDefinition(propagationBehavior);
if (isolationLevel != ISOLATION_DEFAULT) {
def.setIsolationLevel(isolationLevel);
}
TransactionStatus tx = txMgr.getTransaction(def);
ctx.transaction(new SpringTransaction(tx));
}
@Override
public void commit(TransactionContext ctx) {
LOGGER.debug("commit transaction");
txMgr.commit(((SpringTransaction) ctx.transaction()).tx);
}
@Override
public void rollback(TransactionContext ctx) {
LOGGER.error("rollback transaction");
txMgr.rollback(((SpringTransaction) ctx.transaction()).tx);
}
public JooqPlatformTransactionProvider derive(ConnectionProvider provider, int propagationBehavior, int isolationLevel) {
return new JooqPlatformTransactionProvider(provider, txMgr, propagationBehavior, isolationLevel);
}
} The services using JOOQ will not be proxied and we will not use the Transaction attribute (one less aop-proxy). The transactions are controlled by delegates instead: try (var uow = unitOfWorkFactory.create()) {
uow.doWork(() -> result.set(UserDataCmdUtil.save(uow, user)));
} In the JOOQ configuration we are tapping into the PlatformTransactionManager like this: //
@Bean
public org.jooq.Configuration jooqConfig(DatabaseProperties props,
ConnectionProvider connectionProvider,
PlatformTransactionManager transactionManager
) {
SQLDialect sqlDialect = SQLDialect.ORACLE11G;
if (props.sqlDialect == SqlDialect.H2) sqlDialect = SQLDialect.H2;
if (props.sqlDialect == SqlDialect.PostgreSql) sqlDialect = SQLDialect.POSTGRES;
return new DefaultConfiguration()
.derive(connectionProvider)
.derive(new JooqPlatformTransactionProvider(connectionProvider, transactionManager))
.derive(sqlDialect);
} Is this a way to go? Thanks, |
The same applies for reactive transaction management. Our testing shows that mixing spring and JOOQ leads to transactions not being rolled back: Config:
Example code:
The code leads to a successfully inserted record despite the @wilkinsona Please let me know if we should rather create an issue on the spring framework side. |
The transactional operators are unrelated to the |
@michael-simons Sure but my commet is about transactions and JOOQ in general and that the situation is also unclear in the reactive world. |
Boot doesn't yet do anything with jOOQ in a reactive (I assume you're using R2DBC) context and, especially given that you're configuring things yourself, I can't see a connection with Boot. From what you've shared thus far, the only connection to Spring in general seems to be your |
Yep only using the preconfigured |
Hi, Isn't what @sbuettner reported a bug ? I'll gladly open another issue or a Stackoverflow if I'm mistaken, but it seems like a weird behavior with Jooq usage of connectionFactory proxies ? I'm able to have my db request successfully participate in a current Tx if using a connection directly opened from a TransactionAwareConnectionFactoryProxy, but it doesn't seem to work if passing the same factory to settings ? Am I missing something here ? class CommandLineSpringApp(
@Autowired private val transactionalOperator: TransactionalOperator,
@Autowired private val connectionFactory: ConnectionFactory
) : CommandLineRunner {
override fun run(vararg args: String?) {
runBlocking {
getTxIdFromDBTransactional()
getTxIdFromDBTransactional()
}
}
private suspend fun getTxIdFromDBTransactional() =
transactionalOperator.executeAndAwait {
println("With direct connection from TransactionAwareConnectionFactoryProxy = all calls happen in same tx: " + getTxIdJooqDirectConnection())
println("With direct connection from TransactionAwareConnectionFactoryProxy = all calls happen in same tx: " + getTxIdJooqDirectConnection())
println("With direct connection from TransactionAwareConnectionFactoryProxy = all calls happen in same tx: " + getTxIdJooqDirectConnection())
println("It doesn't seem to be when we use TransactionAwareConnectionFactoryProxy: " + getTxIdJooqConnectionFactory())
println("It doesn't seem to be when we use TransactionAwareConnectionFactoryProxy: " + getTxIdJooqConnectionFactory())
println("It doesn't seem to be when we use TransactionAwareConnectionFactoryProxy: " + getTxIdJooqConnectionFactory())
}
private suspend fun getTxIdJooqDirectConnection(): Int {
val reactiveConn = TransactionAwareConnectionFactoryProxy(connectionFactory).create().awaitSingle()
return DSL.using(
reactiveConn
).dsl()
.select(DSL.function("txid_current", SQLDataType.INTEGER))
.fetchOneAwait()
.value1()
}
private suspend fun getTxIdJooqConnectionFactory(): Int =
DSL.using(
DefaultConfiguration()
.set(TransactionAwareConnectionFactoryProxy(connectionFactory))
.set(SQLDialect.POSTGRES)
).dsl()
.select(DSL.function("txid_current", SQLDataType.INTEGER))
.fetchOneAwait()
.value1()
suspend fun <T : Record> ResultQuery<T>.fetchOneAwait(): T =
Mono.from(this).awaitSingle() Output :
Regards, |
It may well be a bug – I don't know enough about jOOQ's support for R2DBC to comment with any degree of certainty – but I would be surprised if it has anything to do with Spring Boot. As I said above, Boot doesn't yet do anything with jOOQ in a reactive context so its involvement is minimal. |
When using Spring Boot with jOOQ, a common source of confusion is how to use transactions. There are mainly three options:
@Transactional
annotations or programmatically, using aTransactionManager
The first case doesn't lead to any integration problems, but the second and third ones do. For example, the
org.springframework.boot.autoconfigure.jooq.SpringTransactionProvider
does not work with jOOQ's asynchronous transactions as can be seen in this issue here: jOOQ/jOOQ#10850 (helpful MCVE here: https://github.com/anushreegupta44/jOOQ-mcve). The workaround is to manually remove theSpringTransactionProvider
again from the jOOQConfiguration
, but users can reasonably expect this to work out of the box, especially when there are no Spring transactions involved.The
SpringTransactionProvider
was originally implemented as a bridge to allow for mixing Spring transactions (e.g. based on@Transactional
annotations), and then in some methods, use jOOQ's transaction APIs, which delegate to Spring, but it's probably not implemented correctly for all cases, nor am I sure whether it should always be added to Spring Boot's jOOQ auto-configuration, because when people want to use jOOQ's transaction APIs, it's not necessary.I'm not sure what the best approach here is to improve the out-of-the-box integration experience for all three of the above cases... My intutition tells me that the
SpringTransactionProvider
should get an instance of theorg.jooq.impl.DefaultTransactionProvider
and delegate to it for all three methods (begin
,commit
,rollback
) whenTransactionSynchronizationManager.isActualTransactionActive()
is false, but that may not be sufficient for asynchronous transactions.Thoughts?
The text was updated successfully, but these errors were encountered: