Annotation @Transactional. How to Rollback

How to do Rollback with Transactional Annotation

You may try @Transactional(rollbackFor = XYZException.class).
XYZException should be an exception which should wrap all the exceptions/exception for which you want to rollback the transaction.

@Transactional doesn't rollback by calling an External method that throw an RuntimeException

Spring 's @Transactional is just a declarative way to manage the underlying JPA transaction. So it depends on the rollback behaviour of JPA which is defined by the spec. as follows :

For both transaction-scoped persistence contexts and for extended
persistence contexts that are joined to the current transaction,
transaction rollback causes all pre-existing managed instances and
removed instances[29] to become detached. The instances' state will be
the state of the instances at the point at which the transaction was
rolled back.
Transaction rollback typically causes the persistence
context to be in an inconsistent state at the point of rollback.

Hibernate docs also mentions the same :

Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction.
This means that the database state and the business objects will be
out of sync.
Usually, this is not a problem because exceptions are
not recoverable and you will have to start over after rollback anyway.

So it is the expected rollback behaviour which the Counter will still have the state just before the transaction is rollback (i.e count=1).

For the entity that is already exist in the DB , you can restore its state back to the same as DB manually by using entityManager.refresh() or simply use entityManager.get() or JPQL to retrieve it again from the DB.

For the entity that does not exist in the DB , simply re-execute the codes to create it again.

So rollback here just means nothing will be updated in DB. It does not mean that it will restore the object state to the moment just before executing a @Transactional method.

Why transactional does not rollback when RuntimeException occur?

Due to way spring proxying works by default, you can not call a "proxied" method from within the instance.

Consider that when you put @Transactional annotation on a method, Spring makes a proxy of that class and the proxy is where the transaction begin/commit/rollback gets handled. The proxy calls the actual class instance. Spring hides this from you.

But given that, if you have a method on the class instance (test()), that calls another method on itself (this.save()), that call doesn't goes through the proxy, and so there is no "@Transactional" proxy. There is nothing to do the rollback when the RuntimeException occurs.

There are ways to change the how Spring does the proxying which would allow this to work, but it has changed over the years. There are various alternatives. One way is to create create separate classes. Perhaps UserServiceHelper. The UserServiceHelper contains @Transactional methods that get called by UserService. This same issue occurs when different @Transactional isolations and propagations are needed.

Related answers and info:

  • Spring @Transaction method call by the method within the same class, does not work?
  • Does Spring @Transactional attribute work on a private method?
  • Spring Transaction Doesn't Rollback

Your example code is not very clear as to what you are trying to do. Often, @Transactional would be put on the service class and apply to all public methods (like test()).

How to rollback with 2 transaction in same method on Spring Boot

This is totally logical what you are trying to do. Just rollingback when one part of your flow fails is normal.

I'll give more background to make sure you have everything:

  • @Transactional only rollback, by default, when a RuntimeException is thrown. You can customize this. Check the doc, it is very easy to find out. Be careful, make sure you use org.springframework.transaction.annotation.Transactional. You have more options with this one.
  • Understanding propagation is key. By default, the propagation of @Transactional is Required, which means it will either joins an existing transaction, or creates a new one. Requires_New always create a new one. In your case, Propagation.REQUIRED is the good one.
  • Once you are in a Transactional method, adding @Transactional on other method inside your class and make inner call won't have any effect. The transaction starts when you enter in your method from the outside of your class. Once you in your class, the others annotations won't affect the runtime.

Three things/questions to consider:

  • What kind of error is throwing the RoleRepository.save(...) ?
  • What is the context ? Is it a test ? Is it a running application ? Could you provide more code ?
  • As said in one comment, read this: Spring Transaction Management with Hibernate and MySQL, Global and Local. You could have some issues with that as well...

Spring boot @Transactional not rolling back the database inserts

There are several things that break proper transactions in Spring

  1. Your service method is private
  2. You are catching and swallowing exceptions

private method

The fact that your PublicationServiceImpl save method is private basically makes the @Transactional on that method useless. As a private method cannot be proxied, no transactions will apply. Even if it would be public it wouldn't work as you are calling the method from within the same object, hence the transactionality of that method applies.

To fix, make your method public and call the save method from an other class (or make the actual method that is calling save have the proper @Transactional.

The fact that is doesn't work is due to the type op AOP being used, by default Spring will use proxies and this is a drawback of using proxy based AOP.

Another solution to make it work with private methods is to switch to full-blown AspectJ with either compile-time or load-time weaving of the classes. Both require additional setup and that can be tedious.

Catch and swallow exceptions

You have in both your repository as well as your service a try/catch block. Each of those catches and swallows the exceptions (they are logged but not re-thrown).

For transactions to work properly it needs to see the exceptions. The fact that you are catching and swallowing them, makes the transaction aspect not see them and instead of doing a rollback, do a commit. For the transaction aspect everything is ok because there was no exception.

To fix, remove either the try/catch or rethrow the exceptions.

Rollback a @Transactional annotated method

You shouldn't call Rollback programmatically. The best way, as recommended by the docs, is to use declarative approach. To do so, you need to annotate which exceptions will trigger a Rollback.

In your case, something like this

@Transactional(rollbackFor={MyException.class, AnotherException.class})
public SomeResult doSomething(){
...
}

Take a look at the @Transaction API and the docs about rolling back a transaction.

If, despite the docs recommendation, you want to make a programmatic rollback, then you need to call it from TransactionAspectSupport as already suggested. This is from the docs:

public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}

There may be a architecture mistake though. If your method fails and you need to throw an exception, you shouldn't expect it to return anything. Maybe you're giving too much responsibilities to this method and should create a separated one that only model data, and throws an exception if something goes wrong, rolling back the transaction. Anyway, read the docs.



Related Topics



Leave a reply



Submit