When using @TransactionalEventListener
with the default AFTER_COMMIT
or AFTER_COMPLETION
phase, any database writes in the listener method are being discard silently, e.g.:
@TransactionalEventListener
void onEvent(BookSavedEvent event) {
// This does get called, but the following write to the database will be discarded silently
var book = books.findById(event.bookId).orElseThrow();
book.title = "New Title";
books.save(book);
}
This problem doesn't happen if the phase is BEFORE_COMMIT
or if just using the regular @EventListener
instead.
I've made an example project using v2.5.0 demonstrating the problem: https://github.com/laech/spring-transactional-event-listener-disappearing-writes/blob/master/src/main/java/com/example/demo/DemoApplication.java
If you do ./gradlew bootRun
on the above project you can see the mismatch in expectation.
Comment From: sbrannen
When your @TransactionalEventListener
method is invoked after the transaction has already been committed, the transaction is still active for the current thread; however, you can no longer commit changes with that transaction.
Behind the scenes, the @TransactionalEventListener
support is implemented via org.springframework.transaction.event.TransactionalApplicationListenerSynchronization
. From the Javadoc for its afterCompletion()
method, we see the following (from TransactionSynchronization.afterCompletion(int)
):
Invoked after transaction commit/rollback. Can perform resource cleanup after transaction completion.
NOTE: The transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still "participate" in the original transaction, allowing to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.
The key point there is "(with no commit following anymore!)".
To use PROPAGATION_REQUIRES_NEW
in your use case, you have to construct a TransactionTemplate
as in the following revised implementation of your BookSavedListener
.
@Component
static class BookSavedListener {
@Autowired
private BookRepository books;
@Autowired
private PlatformTransactionManager txManager;
@TransactionalEventListener
void onEvent(BookSavedEvent event) {
TransactionDefinition txDefinition = new DefaultTransactionDefinition(PROPAGATION_REQUIRES_NEW);
TransactionTemplate txTemplate = new TransactionTemplate(txManager, txDefinition);
txTemplate.executeWithoutResult(status -> {
var book = books.findById(event.bookId).orElseThrow();
book.title = "New Title";
books.save(book);
System.out.println();
System.out.println(books.findAll() + " <- expected");
});
}
}
With the above change, the updated book is in fact saved with the "New Title".
I am therefore closing this issue as "works as designed".
Comment From: sbrannen
Although the information necessary to understand this behavior is documented in the Javadoc for TransactionSynchronization#afterCompletion(int)
and cross referenced from each enum constant in TransactionPhase
, I think we can make this information more easily discoverable by mentioning it directly in TransactionPhase
.
I am therefore reopening this issue to improve the documentation in TransactionPhase
.
Comment From: laech
I think that documentation will need to be highlighted on TransactionalEventListener
itself to make it as obvious as possible.
Ideally the framework should throw an exception in this case if this is an invalid operation, accepting the writes and then silently drops the them is pretty surprising behavior and will be/is the source of obscure bugs.
Comment From: sbrannen
I think that documentation will need to be highlighted on
TransactionalEventListener
itself to make it as obvious as possible.
In 1e1045ba427001c471bed10ac7ddc56f028a6301, I improved the documentation in @TransactionalEventListener
as well as in TransactionPhase
.