4

In SpringBoot using spring data jpa, default configurations (not changing anything), if in my service layer I call the saveAll method of a given repository which is annotated with @Repository, and I have a catch clause that catches DataAccessException which I understand is the one that covers all the possible exceptions that could go wrong with the database, will the rollback still be triggered thanks to the @Transactional annotation?

For example:

Repository:

@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {

}

Service:

@Service
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public void saveAllBooks(List<Book> books) {
        try {
            bookRepository.saveAll(books);
        } catch (DataAccessException e) {
            // notify external service about failure
            // log exception
        }
    }

}

If I do this, the rollback will still happen or do I need to rethrow the exception? I need to guarantee the atomicity of this kind of transaction, save all the books or not save any of the books. Thanks in advance :)

v.ladynev
  • 19,275
  • 8
  • 46
  • 67
BugsOverflow
  • 386
  • 3
  • 19
  • 1
    This answer should be what you are looking for: https://stackoverflow.com/questions/25738883/spring-transactional-annotation-when-using-try-catch-block – Kaiak Apr 20 '23 at 11:37
  • 1
    It's a shame that this question has been closed. It is about JPA and transactions. The linked question is only partially useful for JPA. – Mar-Z Apr 20 '23 at 11:59
  • 1
    @Mar-Z Agree. fixed :) – v.ladynev Apr 20 '23 at 12:00

3 Answers3

2

First thing what need a clarification. As you are using Spring Data JPA then the annotation @Repository is irrelevant and will be ignored. Spring will instead use the EnableJpaRepositories and configure a proxy of SimpleJPARepository for every interface which extends the JPARepository. What your BookRepository does. In consequence all methods of this repository will be transactional.

All exceptions thrown in these methods will be translated by Spring and:

  • any unchecked exception (like DataAccessException, IndexOutOfBoundsException) will trigger rollback of the transaction
  • any checked exception (like SQLException) will NOT trigger rollback

This will happen independent and before the try/catch logic.

Mar-Z
  • 2,660
  • 2
  • 4
  • 16
  • I still have something that need clarification, if for example in my BookService I am using 2 repositories, for example AuthorRepository and also BookRepository, let suppose that I save the books but after I want to also save the Author, but this one fails, I want it to rollback all, the books saving and also the possible Author saving failure, here is where I would need the service to annotate the method with Transactional for it to rollback ALL in case BookRepo or AuthorRepo fails right? I can writre an example in my question if you want – BugsOverflow Apr 20 '23 at 12:06
  • 1
    @BugsOverflow Better to create another question – v.ladynev Apr 20 '23 at 12:14
  • 1
    @BugsOverflow you are right. Service class is the right place to implement such chained logic and to use an (additional) transaction scope. – Mar-Z Apr 20 '23 at 12:27
  • Thanks for your help, I posted an answer with some more clarification because I really wanted to see if the theory I understood works like I think, feel free to check it out and leave feedback / ask anything – BugsOverflow May 01 '23 at 16:37
1

saveAll() method is implemented in the SimpleJpaRepository class.

    @Transactional
    @Override
    public <S extends T> List<S> saveAll(Iterable<S> entities) {

    }

It has @Transactional annotation. Your service method doesn't have @Transactional annotation. So it means the rollback will still happen, even you catch an exception. You don't need to rethrow an exception in such case.

Otherwise if you put @Transactional to your saveAllBooks() method, rollback will not happen. The reason that the annotation from saveAllBooks() cancel the annotation from saveAll().

    @Transactional
    public void saveAllBooks(List<Book> books) {
        try {
            bookRepository.saveAll(books);
        } catch (DataAccessException e) {
            // notify external service about failure
            // log exception
        }
    }
v.ladynev
  • 19,275
  • 8
  • 46
  • 67
  • Oh this is what I was expecting, if I wanted to still rollback with the Transactional at my service method (saveAllBooks), I would need to do it programatically right? put in the catch the sentence "TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();" – BugsOverflow Apr 20 '23 at 12:03
  • @BugsOverflow Maybe just rethrow an exception? – v.ladynev Apr 20 '23 at 12:09
  • But how will it work with just rethrowing it? Will it get intercepted with the rollbackFor of the Transactional annotation? would have to specify which exception to rollbackfor which is the one I would throw inside that catch – BugsOverflow Apr 20 '23 at 12:11
  • 1
    @BugsOverflow `DataAccessException` is a runtime exception. You don't need to do anything to have a rollback with it. – v.ladynev Apr 20 '23 at 12:13
  • Ahh okay I think I get it, in case I just rethrow it, it gets handled automagically by the Transactional annotation, because it is runtime exception, same if I throw a different custom exception from the catch, as long as it extends runtime exception it will get automagically rollbacked, but in case I wish to re throw a custom checked exception, there is where I would have to specify in the rollbackFor this custom checked exception right? – BugsOverflow Apr 20 '23 at 12:16
  • @BugsOverflow Sorry. I never use such approach. I would like to advice you to always use runtime exceptions. – v.ladynev Apr 20 '23 at 16:43
  • Thanks for your help, I posted an answer with some more clarification because I really wanted to see if the theory I understood works like I think, feel free to check it out and leave feedback / ask anything – BugsOverflow May 01 '23 at 16:37
1

In adittion to the answers I received, I want to add the following tests I did to be 100% sure I understand the behaviour so here it goes:

The method to test is this one:

@Transactional
public String execute() {
    lock();
    try {
        saveEntities(entities);
        doSomethingElse();
        return "OK";
    } finally {
        unlock();
    }
}

Test #1: What happens if everything goes well but at the finally clause, the unlock method fails and throws a RunTimeException() Answer: It rollbacks the transaction, even more interesting, if you check the database while the finally is executing, you wont see any entities saved yet, it will not save anything until all the function finishes (correctly not abruptly) including the finally.

Test #2: What happens if everything goes well but at the finally clause, the unlock methods fails and throws a RunTimeException(), BUT you catch it there. Answer: It does not rollback the transaction, because since nothing was thrown without being controlled/catched, then everything finishes OK. This example would look like so:

@Transactional
public String execute() {
    lock();
    try {
        saveEntities(entities);
        doSomethingElse();
        return "OK";
    } finally {
        try {
            unlock();
        } catch (RuntimeException e) {
            log.info("Catched the problem from finally so no rollback happens");
        }
    }
}

I did several more test but with this 2, we can conclude that the @Transactional will make sure of 2 things: the code inside the try{} body must execute without any problems (Exceptions/RunTimeExceptions) for it to not rollback, THEN the finally clause code must also execute without any problem for it to not rollback.

If anything goes wrong inside the try{} body, it will already be marked as a Transaction to rollback, no matter what happens in the finally, if it fails inside the try{} body and then it also fails at the finally it will still rollback, even if you catch the problem from the finally, since the try is already finished abrupted/error

So I hope this helps clarify, if you do not use @Transactional it has a completely different behaviour, for example without Transactional if your code in the body of the try{} goes well, you will se the records in your database already, but if the finally goes wrong you will still see them. Another scenario is that if you do not use @Transactional and anything goes wrong in the body of the try{} then you will not see anything in the database (it will "rollback") even if you do not use Transactional, thanks to the Transactional annotations that JpaRepositories already use (Spring), this would be OK if you did not depend on the code that executes on finally, but in my case it is important that this code also succeds.

Also if you are wondering how I managed to see this in the database, I used Thread.sleep after the save method, when catching the Exceptions, etc. That is how I could validate the theory step by step

If you have any doubts feel free to post I will answer, if you imagine any scenario I probably did the test so feel free to ask

BugsOverflow
  • 386
  • 3
  • 19