0

Context

We have a Spring Boot REST application using JPA over Hibernate (Spring Boot 2.0.x, spring-tx 5.0.x, Hibernate 5.2.x). Underlying database is Oracle.

Our architecture doesn't expose JPA entities, instead the service layer converts entities to DTO model classes, which are exposed to the REST controller layer. To make updates, we don't use auto-flushing, but always make an explicit call to service class CRUD methods, which translate to saveAndFlush() or deleteById() on the repository (for create/update or delete, respectively).

Objective

What we want is:

  1. Read-only operations execute non-transactionally
  2. Write operations (single or multiple) execute within a single transaction
  3. We are alerted if we forget to declare a transaction on a write operation.

Partial solution

To do this, we have:

  • Repository super interface is annotated with @Transactional(propagation=SUPPORTS, readOnly=true) which covers point 1.

  • Service classes are not annotated, and service write methods are annotated with `@Transactional(propagation=REQUIRED), which covers point 2.

  • Repository saveAndFlush(entity) and deleteById(id) methods (the only write methods currently called from services) are annotated with @Transactional(propagation=MANDATORY) which causes an exception to be thrown if they are executed outside of a transaction context.

So regarding point 3, we are fine if someone defines a new service method that writes data, and forgets to apply @Transactional on it: an exception will be thrown.

But if someone uses another repository method that writes data, and forgets to annotate it at service level, it will get no transaction from the service level, and execute within the repository's default (class-level) transactionality, that's to say: SUPPORTS. In this case, with no ongoing transaction, the readOnly flag will be ignored, and the write will be done silently.

Note that Hibernate's flushMode is set to MANUAL when the readOnly flag is set on @Transactional. If we had REQUIRED propagation, the write would be silently ignored (which would not be acceptable either), whereas in our context of SUPPORTS propagation, the write is silently performed.


Question

Is there any way we can set up Spring transactions / JPA / Hibernate so that our repository permits non-transactional reads, but throws an exception when any data is written, unless via one of the methods that we've overridden to declare @Transactional(propagation=MANDATORY)?

As noted above, readOnly=true does not achieve this, even though it does set Hibernate's flushMode.

Andrew Spencer
  • 15,164
  • 4
  • 29
  • 48
  • Whilst writing up, I also realized we have an issue if someone writes a service method that composes CRUD calls, and doesn't annotate it, because the create/update/delete service methods are annotated, and would run in separate transactions. But I'll avoid the temptation to ask both questions at the same time. – Andrew Spencer Oct 05 '18 at 09:06
  • 1
    Not an answer, but the idea that read operations shouldn't be executed inside a transaction is a really bad idea. If you execute two subsequent reads out of a transaction, they will see two separate snapshots of the database, and potentially return incoherent data. And of course, they will also use two different first-layer caches, which means that you will execute the same queries sevral times instead of benefitting from the cache. Use transactions for **all** your use-cases. See https://stackoverflow.com/a/26327536/571407 for a longer explanation, from the author of the Hibernate doc. – JB Nizet Oct 05 '18 at 09:10
  • @JBNizet I was aware that subsequent reads would see data resulting potentially from different states of the DB (commits from other sessions) and it's acceptable in our context. But I hadn't considered the L1 cache issue, and this is really important especially with our patterns of data access. The more so considering we're using Oracle, so don't have to worry about read locks on the lines we read within the transaction. Thanks for the tip. – Andrew Spencer Oct 05 '18 at 09:32
  • @JBNizet funny, in the linked answer a commenter references the exact same article that I read which made me believe we didn't want transactions on read operations. – Andrew Spencer Oct 05 '18 at 09:34
  • @JBNizet also relevant: by default, in a WEB context, Spring Boot uses the Open EntityManager In View pattern which means all data access from 1 HTTP request is done via the same L1 cache – Andrew Spencer Oct 05 '18 at 10:02

1 Answers1

1

It’s a try if a validating TransactionManager does the trick: https://info.michael-simons.eu/2018/09/25/validate-nested-transaction-settings-with-spring-and-spring-boot/

import org.springframework.boot.autoconfigure.transaction.PlatformTransactionManagerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;

@Configuration
class TransactionManagerConfiguration {

    @Bean
    public PlatformTransactionManagerCustomizer<AbstractPlatformTransactionManager> transactionManagementConfigurer() {
        return (AbstractPlatformTransactionManager transactionManager) -> transactionManager
            .setValidateExistingTransaction(true);
    }
}
Michael Simons
  • 4,640
  • 1
  • 27
  • 38