Validate nested Transaction settings with Spring and Spring Boot
The Spring Framework has had an outstanding, declarative Transaction management for years now.
The configurable options maybe overwhelming at first, but important to accommodate many different scenarios.
Three of them stick out: propagation, isolation and to some lesser extend, read-only mode (more on that a bit later)
- propagation describes what happens if a transaction is to be opened inside the scope of an already existing transaction
- isolation determines among other whether one transaction can see uncommitted writes from another
- read-only can be used as a hint when user code only executes reads
I wrote “to some lesser extend” regarding read-only as read-only transactions can be a useful optimization in some cases, such as when you use Hibernate. Some underlying implementations treat them as hints only and don’t actually prevent writes. For a full description of things, have a look at the reference documentation on transaction strategies.
Note: A great discussion on how setting read-only to true can affect performance in a positive way with Spring 5.1 and Hibernate 5.3 can be find in the Spring Ticket SPR-16956.
Some of the transactions settings are contradicting in case of nested transaction scenarios. The documentation says:
By default, a participating transaction joins the characteristics of the outer scope, silently ignoring the local isolation level, timeout value, or read-only flag (if any).
This service here is broken in my perception. It explicitly declare a read-only transaction and than calls a save
on a Spring Data repository:
import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service; @Service public class BrokenService { private final ThingRepository thingRepository; public BrokenService(ThingRepository thingRepository) { this.thingRepository = thingRepository; } @Transactional(readOnly = true) public ThingEntity tryToWriteInReadOnlyTx() { return this.thingRepository.save(new ThingEntity("A thing")); } } |
This can be detected by using a PlatformTransactionManager
that supports validation of existing transactions. The JpaTransactionManager
does this as well as Neo4js Neo4jTransactionManager
(both extending AbstractPlatformTransactionManager
).
To enable validation for JPA’s transaction manager in a Spring Boot based scenario, just make use of the provided PlatformTransactionManagerCustomizer
interface. Spring Boots autoconfiguration calls them with the corresponding transaction manager:
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); } } |
In the unlikely scenario you’re not using Spring Boot, you can always let Spring inject an EntityManagerFactory
or for example Neo4j’s SessionFactory
and create and configure the corresponding transaction manager yourself. My Neo4j tip does cover that as well.
If you try to execute the above service now, it’ll fail with an IllegalTransactionStateException
indicating “save is not marked as read-only but existing transaction is”.
The question if a validation is possible arose in a discussion with customers. Funny enough, even working with Spring now for nearly 10 years, I never thought of that and always assumed it would validate those automatically but never tested it. Good to have learned something new, again.
Featured image on this post: Transaction by Nick Youngson CC BY-SA 3.0 Alpha Stock Images.