Saturday, December 21, 2024
HomeProgrammingNested Transactions in jOOQ – Java, SQL and jOOQ.

Nested Transactions in jOOQ – Java, SQL and jOOQ.


Since jOOQ 3.4, now we have an API that simplifies transactional logic on high of JDBC in jOOQ, and ranging from jOOQ 3.17 and #13502, an equal API will even be made accessible on high of R2DBC, for reactive purposes.

As with all the pieces jOOQ, transactions are applied utilizing express, API primarily based logic. The implicit logic applied in Jakarta EE and Spring works nice for these platforms, which use annotations and points all over the place, however the annotation-based paradigm doesn’t match jOOQ properly.

This text reveals how jOOQ designed the transaction API, and why the Spring Propagation.NESTED semantics is the default in jOOQ.

Following JDBC’s defaults

In JDBC (as a lot as in R2DBC), a standalone assertion is all the time non-transactional, or auto-committing. The identical is true for jOOQ. When you go a non-transactional JDBC connection to jOOQ, a question like this might be auto-committing as properly:

ctx.insertInto(BOOK)
   .columns(BOOK.ID, BOOK.TITLE)
   .values(1, "Starting jOOQ")
   .values(2, "jOOQ Masterclass")
   .execute();

Thus far so good, this has been an inexpensive default in most APIs. However often, you don’t auto-commit. You write transactional logic.

Transactional lambdas

If you wish to run a number of statements in a single transaction, you possibly can write this in jOOQ:

// The transaction() name wraps a transaction
ctx.transaction(trx -> {

    // The entire lambda expression is the transaction's content material
    trx.dsl()
       .insertInto(AUTHOR)
       .columns(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
       .values(1, "Tayo", "Koleoso")
       .values(2, "Anghel", "Leonard")
       .execute();

    trx.dsl()
       .insertInto(BOOK)
       .columns(BOOK.ID, BOOK.AUTHOR_ID, BOOK.TITLE)
       .values(1, 1, "Starting jOOQ")
       .values(2, 2, "jOOQ Masterclass")
       .execute();

    // If the lambda is accomplished usually, we commit
    // If there's an exception, we rollback
});

The psychological mannequin is strictly the identical as with Jakarta EE and Spring @Transactional points. Regular completion implicitly commits, distinctive completion implicitly rolls again. The entire lambda is an atomic “unit of labor,” which is fairly intuitive.

You personal your management stream

If there’s any recoverable exception within your code, you’re allowed to deal with that gracefully, and jOOQ’s transaction administration gained’t discover. For instance:

ctx.transaction(trx -> {
    strive {
        trx.dsl()
           .insertInto(AUTHOR)
           .columns(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
           .values(1, "Tayo", "Koleoso")
           .values(2, "Anghel", "Leonard")
           .execute();
    }
    catch (DataAccessException e) {

        // Re-throw all non-constraint violation exceptions
        if (e.sqlStateClass() != C23_INTEGRITY_CONSTRAINT_VIOLATION)
            throw e;

        // Ignore if we have already got the authors
    }

    // If we had a constraint violation above, we will proceed our
    // work right here. The transaction is not rolled again
    trx.dsl()
       .insertInto(BOOK)
       .columns(BOOK.ID, BOOK.AUTHOR_ID, BOOK.TITLE)
       .values(1, 1, "Starting jOOQ")
       .values(2, 2, "jOOQ Masterclass")
       .execute();
});

The identical is true in most different APIs, together with Spring. If Spring is unaware of your exceptions, it won’t interpret these exceptions for transactional logic, which makes good sense. In spite of everything, any third get together library might throw and catch inner exceptions with out you noticing, so why ought to Spring discover.

Transaction propagation

Jakarta EE and Spring supply a wide range of transaction propagation modes (TxType in Jakarta EE, Propagation in Spring). The default in each is REQUIRED. I’ve been attempting to analysis why REQUIRED is the default, and never NESTED, which I discover way more logical and proper, as I’ll clarify afterwards. If you already know, please let me know on twitter or within the feedback:

My assumption for these APIs is

  1. NESTED requires SAVEPOINT assist, which isn’t accessible in all RDBMS that assist transactions
  2. REQUIRED avoids SAVEPOINT overhead, which generally is a downside should you don’t truly must nest transactions (though we would argue that the API is then wrongly annotated with too many incidental @Transactional annotations. Similar to you shouldn’t mindlessly run SELECT *, you shouldn’t annotate all the pieces with out giving issues sufficient thought.)
  3. It isn’t unlikely that in Spring consumer code, each service methodology is simply blindly annotated with @Transactional with out giving this matter an excessive amount of thought (identical as error dealing with), after which, making transactions REQUIRED as a substitute of NESTED would simply be a extra handy default “to make it work.” That will be in favour of REQUIRED being extra of an incidental default than a properly chosen one.
  4. JPA can’t truly work properly with NESTED transactions, as a result of the entities change into corrupt (see Vlad’s touch upon this). For my part, that’s only a bug or lacking function, although I can see that implementing the function may be very complicated and maybe not value it in JPA.

So, for all of those merely technical causes, it appears to be comprehensible for APIs like Jakarta EE or Spring to not make NESTED the default (Jakarta EE doesn’t even assist it in any respect).

However that is jOOQ and jOOQ has all the time been taking a step again to consider how issues must be, quite than being impressed with how issues are.

When you consider the next code:

@Transactional
void tx() {
    tx1();

    strive {
        tx2();
    }
    catch (Exception e) {
        log.data(e);
    }

    continueWorkOnTx1();
}

@Transactional
void tx1() { ... }

@Transactional
void tx2() { ... }

The intent of the programmer who wrote that code can solely be one factor:

  • Begin a worldwide transaction in tx()
  • Do some nested transactional work in tx1()
  • Strive doing another nested transactional work in tx2()
    • If tx2() succeeds, fantastic, transfer on
    • If tx2() fails, simply log the error, ROLLBACK to earlier than tx2(), and transfer on
  • No matter tx2(), proceed working with tx1()‘s (and probably additionally tx2()‘s) final result

However this isn’t what REQUIRED, which is the default in Jakarta EE and Spring, will do. It would simply rollback tx2() and tx1(), leaving the outer transaction in a really bizarre state, which means that continueWorkOnTx1() will fail. However ought to it actually fail? tx2() was alleged to be an atomic unit of labor, impartial of who known as it. It isn’t, by default, so the Exception e should be propagated. The one factor that may be carried out within the catch block, earlier than mandatorily rethrowing, is clear up some assets or do some logging. (Good luck ensuring each dev follows these guidelines!)

And, as soon as we mandatorily rethrow, REQUIRED turns into successfully the identical as NESTED, besides there are not any extra savepoints. So, the default is:

  • The identical as NESTED within the glad path
  • Bizarre within the not so glad path

Which is a robust argument in favour of constructing NESTED the default, no less than in jOOQ. Now, the linked twitter dialogue digressed fairly a bit into architectural issues of why:

  • NESTED is a foul concept or doesn’t work all over the place
  • Pessimistic locking is a foul concept
  • and so forth.

I don’t disagree with a lot of these arguments. But, focusing solely on the listed code, and placing myself within the sneakers of a library developer, what might the programmer have probably supposed by this code? I can’t see something different that Spring’s NESTED transaction semantics. I merely can’t.

jOOQ implements NESTED semantics

For the above causes, jOOQ’s transactions implement solely Spring’s NESTED semantics if savepoints are supported, or fail nesting totally in the event that they’re not supported (weirdly, this isn’t an choice in both Jakarta EE and Spring, as that might be one other affordable default). The distinction to Spring being, once more, that all the pieces is completed programmatically and explicitly, quite than implicitly utilizing points.

For instance:

ctx.transaction(trx -> {
    trx.dsl().transaction(trx1 -> {
        // ..
    });

    strive {
        trx.dsl().transaction(trx2 -> {
            // ..
        });
    }
    catch (Exception e) {
        log.data(e);
    }

    continueWorkOnTrx1(trx);
});

If trx2 fails with an exception, solely trx2 is rolled again. Not trx1. In fact, you possibly can nonetheless re-throw the exception to roll again all the pieces. However the stance right here is that should you, the programmer, inform jOOQ to run a nested transaction, properly, jOOQ will obey, as a result of that’s what you need.

You couldn’t probably need anything, as a result of then, you’d simply not nest the transaction within the first place, no?

R2DBC transactions

As talked about earlier, jOOQ 3.17 will (lastly) assist transactions additionally in R2DBC. The semantics is strictly the identical as with JDBC’s blocking APIs, besides that all the pieces is now a Writer. So, now you can write:

Flux<?> flux = Flux.from(ctx.transactionPublisher(trx -> Flux
    .from(trx.dsl()
        .insertInto(AUTHOR)
        .columns(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
        .values(1, "Tayo", "Koleoso")
        .values(2, "Anghel", "Leonard"))
    .thenMany(trx.dsl()
        .insertInto(BOOK)
        .columns(BOOK.ID, BOOK.AUTHOR_ID, BOOK.TITLE)
        .values(1, 1, "Starting jOOQ")
        .values(2, 2, "jOOQ Masterclass"))
}));

The instance makes use of reactor as a reactive streams API implementation, however you may also use RxJava, Mutiny, or no matter. The instance works precisely the identical because the JDBC one, initially.

Nesting additionally works the identical method, within the common, reactive (i.e. extra laborious) method:

Flux<?> flux = Flux.from(ctx.transactionPublisher(trx -> Flux
    .from(trx.dsl().transactionPublisher(trx1 -> { ... }))
    .thenMany(Flux
        .from(trx.dsl().transactionPublisher(trx2 -> { ... }))
        .onErrorContinue((e, t) -> log.data(e)))
    .thenMany(continueWorkOnTrx1(trx))
));

The sequencing utilizing thenMany() is only one instance. You could discover a want for totally completely different stream constructing primitives, which aren’t strictly associated to transaction administration.

Conclusion

Nesting transactions is sometimes helpful. With jOOQ, transaction propagation is far much less of a subject than with Jakarta EE or Spring as all the pieces you do is often express, and as such, you don’t unintentionally nest transactions, if you do, you do it deliberately. That is why jOOQ opted for a special default than Spring, and one which Jakarta EE doesn’t assist in any respect. The Propagation.NESTED semantics, which is a robust approach to preserve the laborious savepoint associated logic out of your code.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments