Spring #Transactional. How does it really work? - java

We are using Spring and Hibernate in our project. We are using Spring transactional annotations and we understand what it does, but we still have some doubts regarding rollbacks.
Let's say we have the following code.
#Transactional
#Service
public class PersonServiceImpl implements PersonService {
#Autowired
private PersonDao personDao;
#Autowired
private AnotherService anotherService;
#Override
public void doSomething() throws Exception {
Person person = new Person();
person.setName("John");
personDao.insert(person);
person.setName("Megan");
//What happens if the following call fails and throws and exception.
personDao.update(person);
// What happens if this other service's call fails?
anotherService.doSomethingElse();
}
}
If either the update or the other service's call fails, what would happen to the insert? Would there be a rollback and the insert would never be executed? Or would the insert still persist in the DB? Do I need to declare the following command in each method for the rollback to be executed.
#Transactional(rollbackFor= Exception.class)
If that is the case, what happens if my service call throws the exception? Would it still works? Thanks for the help. I would really liked to understand the way rollbacks are executed inside a service call.

By default if the exception is checked (extends Exception), the rollback won't happen - the transaction will be committed. Otherwise (if defaults were changed or we're talking about unchecked exceptions), the transaction is rolled back and INSERT/UPDATE statements don't persist anything. BTW, this is described in the JavaDocs to the rollbackFor attribute ;)
If the exception is eventually thrown from your Transactional method it doesn't matter where that exception originated.
If you're not sure about the behaviour, you can always debug Spring's HibernateTransactionManager and its superclasses.

Related

RuntimeException in child function should not affect parent calling function - does REQUIRES_NEW play a role?

I'm working with a DB2 database and tested following code:
no matter methodB has Propagation.REQUIRES_NEW or not, if methodB has exception, methodA's result will be committed correctly regardless.
This is against my assumption that Propagation.REQUIRES_NEW must be used to achieve this.
ClassA
#Autowire
private ClassB classB;
#Transactional
methodA(){
...
try{
classB.methodB();
}catch(RuntimeException ex){
handleException(ex);
}
...
}
ClassB
#Transactional(propagation = Propagation.REQUIRES_NEW)
methodB(){...}
Thanks for #Kayaman I think I figured it out now.
The behaviour I saw is because methodB's #Transactional annotation didn't work, so methodB is treated as a normal function without any transaction annotation.
Where it went wrong is that in methodA, I called methodB from a subclasss of ClassB by super.methodB() and thought that it will give a transactional methodB, which isn't working:
#Service
#Primary
ClassC extends ClassB{
#override
methodB(){
super.methodB();
}
}
I know that transaction annotation will not work if you call a transaction method from another non-transactional method of the same class.
Didn't know that super.methodB() will also fail for the same reason (anyone can give a bit more explanation pls?)
In conclusion, in the example of the first block of code, when methodB has RuntimeException,
If methodB has NO transaction annotation: A & B share the same transaction; methodA will NOT rollback
if methodB has REQUIRED annotation: A & B share the same transaction; methodA will rollback
if methodB has REQUIRES_NEW annotation: A & B have separate transactions; methodA will NOT rollback
Without REQUIRES_NEW (i.e. the default REQUIRED or one of the others that behaves in a similar way), ClassB.methodB() participates in the same transaction as ClassA.methodA(). An exception in in methodB() will mark that same transaction to be rolled back. Even if you catch the exception, the transaction will be rolled back.
With REQUIRES_NEW, the transaction rolled back will be particular to methodB(), so when you catch the exception, there's still the healthy original non-rolled back transaction in existence.
ClassA
#Transactional
methodA(){
try{
classB.methodB();
}catch(RuntimeException ex){
handleException(ex);
}
}
ClassB
#Transactional
methodB(){
throw new RuntimeException();
}
The above code will rollback the whole transaction. With propagation=TransactionPropagation.REQUIRES_NEW for methodB() it will not.
Without any annotation for methodB(), there will be only one tx boundary at methodA() level and Spring will not be aware that an exception is thrown, since it's caught in the middle of the method. This is similar to inlining the contents of methodB() to methodA(). If that exception is a non-database exception (e.g. NullPointerException), the transaction will commit normally. If that exception is a database exception, the underlying database transaction is set to be rolled back, but Spring isn't aware of that. Spring then tries to commit, and throws an UnexpectedRollbackException, because the database won't allow the tx to be committed.
Leaving the annotation out explicitly or accidentally is wrong. If you intend to perform db operations you must be working with a well defined transaction context, and know your propagation.
Calling super.methodB() bypasses Spring's normal proxying mechanism, so even though there is an annotation, it's ignored. Finally, calling super.methodB() seems like a design smell to me. Using inheritance to cut down on lines is often bad practice, and in this case caused a serious bug.

Rollback for doubly nested transaction bypasses savepoint

It's not exactly as the title says, but close to. Consider these Spring beans:
#Bean
class BeanA {
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = EvilException.class)
public void methodA() {
/* ... some actions */
if (condition) {
throw new EvilException();
}
}
}
#Bean
class BeanB {
#Autowired private BeanA beanA;
final int MAX_TRIES = 3;
#Transactional(propagation = Propagation.NESTED)
public void methodB() {
// prepare to call Bean A
try {
beanA.methodA();
/* maybe do some more things */
}
catch (EvilException e) {
/* recover from evil */
}
}
}
#Bean
class MainWorkerBean {
#Autowired private BeanB beanB;
#Autowired private OtherBean otherBean;
#Transactional(propagation = Propagation.REQUIRED)
public void doSomeWork() {
beanB.methodB();
otherBean.doSomeWork();
}
}
Important note: I'm using JDBC transaction manager that supports savepoints.
What I'm expecting this to do is, when EvilException is thrown, the transaction of the BeanA is rolled back, which with this setup happens to be the savepoint created by starting methodB. However, this appears to not be the case.
When going over with debugging tools, what I'm seeing is this:
When doSomeWork of MainWorkerBean starts, new transaction is created
When methodB starts, transaction manager properly initializes a savepoint and hands it to TransactionInterceptor
When methodA starts, transaction manager sees Propagation.REQUIRED again, and hands out a clean reference to the actual JDBC transaction again, that has no knowledge of the savepoint
This means that when exception is thrown, TransactionStatus::hasSavepoint return false, which leads to roll back of the whole global transaction, so recovery and further steps are as good as lost, but my actual code has no knowledge of the rollback (since I've written recovery for it).
For now, I can't consider changing BeanA's transaction to Propagation.NESTED. Admittedly, looks like it's going to allow me to have the more local rollback, but it's going to be too local, because as I understand it, Spring then will have two savepoints, and only roll back the BeanA savepoint, not BeanB one, as I'd like.
Is there anything else I'm missing, such as a configuration option, that would make internal transaction with Propagation.REQUIRED consider that it is running inside a savepoint, and roll back to savepoint, not the whole thing?
Right now we're using Spring 4.3.24, but I already crawled through their code and can't spot any relevant changes, so I don't think upgrading will help me.
As described in this bug ticket: https://github.com/spring-projects/spring-framework/issues/11234
For spring versions < 5.0, in the situation described, the global transaction is set to 'rollback-only'.
In this transaction I am processing several tasks. If an error should occur during a single task, I do not want the whole transaction to be rolled back, therefore I wrap the task processing in another transaction boundary with a propagation of PROPAGATION_NESTED.
The problem comes when, during task processing, calls are made to existing service methods defined with a transaction boundary of PROPAGATION_REQUIRED. Any runtime exceptions thrown from these methods cause the underlying connection to be marked as rollback-only, rather than respecting the current parent transaction nested propagation.
[...]
As of Spring Framework 5.0, nested transactions resolve their rollback-only status on a rollback to a savepoint, not applying it to the global transaction anymore.
On older versions, the recommended workaround is to switch globalRollbackOnParticipationFailure to false in such scenarios.
However, even for Spring5, I noticed when reproducing the problem, that the nested transaction may be rolled back, including all things done in the catch block of methodB(). So your recover code might not work inside methodB(), depending on what your recovery looks like. If methodA() was not transactional, that would not happen. Just something to watch out for.
Some more details to be found here: https://github.com/spring-projects/spring-framework/issues/8135

Spring Data: JPA and Nested Transaction

I am having a problem with Spring JPA Data and nested transactions. Following are two methods with a nested transaction of my service.
#Service
public UserService {
#Transactional
public User createUser(UserDto userDto) {
....
user = saveUser(user);
sendEmail(user);
....
}
#Transactional(propagation = Propagation.REQUIRES_NEW)
public User saveUser(User user) {
return userRepository.save(user);
}
It happens there is one scenario that the method userRepository.save() should throw an exception but somehow is not being thrown, it looks like it is waiting the parent transaction to be finished. I was expecting the exception being thrown on the saveUser method and the sendEmail method not even to be executed.
Because the method UserService.saveUser have the propagation set to Propagation.REQUIRES_NEW I was expecting that transaction to be commited (the SQL statement to be executed) and any exception being propagated.
I did not setup anything related with Transaction, so i believe that the flush mode is set to AUTO.
Can anyone spot what i am doing wrong or what is my misconception?
It's because you're invoking #Transactional method from within same bean.
#Transactional only works on methods invoked on proxies created by spring. It means, that when you create a #Service or other bean, method called from the outside will be transactional. If invoked from within bean, nothing will happen, as it doesn't pass through proxy object.
The easiest solution would be to move the method to another #Service or bean. If you really want to keep it within same component, then you need to invoke it, so that it gets wrapped in proxy by spring AOP. You can do this like that:
private YourClass self;
#Autowired
private ApplicationContext applicationContext;
#PostConstruct
public void postContruct(){
self = applicationContext.getBean(YourClass.class);
}
Then invoking method on self would result in opening a transaction.
In other words: you are not experiencing any of those anomalies, because #Transactional over saveUser does not work.

Should I put #Transactional annotation for submethods also in spring?

I have a main DB handler method, which calls other methods, which are also working with BD things.
I put #Transactional annotation for the main method, because I want to roll back everything, if something goes wrong.
My question is: should I put this annotation also for the submethods, or it will know that the submethods were called from a method which is transactional.
For example, in the deleting method an exception occurs, how can I make sure that the writing part will be also rollbacked:
#Transactional
public void maintDbTings() {
writing();
deleting();
}
#Transactional //do I need this?
public void writing() {
//no exceptions
}
#Transactional //do I need this?
public void deleting() {
//exception occurs
}
Spring begins a transaction when it encounters a method annotated with #Transactional. The transaction’s scope
covers the execution of that method, the execution of any methods that method invokes, and
so on, until the method returns. Any managed resources that are covered by the configured
PlatformTransactionManager and that you use during the transaction scope participate in the
transaction. For example, if you use the org.springframework.jdbc.datasource.DataSourceTransactionManager, a Connection retrieved from the linked DataSource
participates in the transaction automatically.
The transaction terminates one of two ways: Either the method completes execution directly and the transaction manager commits the transaction, or the method throws an exception and the transaction manager rolls the transaction back.
I hope it is clear now.
In plain english, when you have this:
#Transactional
public void maintDbTings() {
writing();
}
#Transactional //do I need this?
public void writing() {
//no exceptions
}
And call mainDbTings, the #Transactional on the writing has no effect. Meaning that the transaction that was started for mainDbThings will still be present/open in writing. So in this case you can easily drop it.
On the other hand since writing is public someone might call it expecting it to be transactional, since it is a service class most probably. In this case making writing to be #Transactional is mandatory and you can't drop it.
So it's up your needs really.
You can use propagation properties like REQUIRED, REQUIRES_NEW, NESTED according to your requirement as described in the below link:
http://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html

Why Spring doesn't have rollback methods in TransactionSynchronization interface?

In Spring TransactionSynchronization interface it has methods (in order of execution):
- beforeCommit
- beforeCompletion
- afterCommit: Can perform further operations right after the main transaction has successfully committed.
- afterCompletion
Why Spring doesn't have rollback methods, such as beforeRollback or afterRollback but it has for commit only (beforeCommit and afterCommit)? Will this is necessary? Can anyone give me some advices or explains about this?
If I want to continue further operations that are supposed to follow on a successful rollback of the main transaction, like notification messages or emails what should I do in this case?
#Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
logger.trace("Rolled back...");
}
}
Can I ask why you are using this interface?
This is used for callbacks by the PlatformTransactionManager which is in charge of managing the transaction lifecycle.
The "normal" use of Spring Transactions is to utilise #Transactional annotations, aop declarations in xml or TransactionTemplate / PlatformTransactionManager programatically to set the transaction scope, behaviour and visibility, as described in the docs - here.
To manage a transaction behaviour for rollbacks etc you just tell spring what to do in the event of an Exception
public class Foo {
#Autowired public Service someService;
#Transactional(propagation = Propagation.REQUIRES_NEW,
noRollbackFor = {IOException.class})
public boolean bar(SomeObject someObject) throws IOException {
someService.doComplicatedThing(someObject.getValue());
}
}
This tells the PlatformTransactionManager to start a new TX when hitting the foo() method, commit on successful return or an IOException, and rollback if there is some other type of exception. This is the genius of it - you dont need to worry about littering your code with getTransaction().isActive() and complicated checks for managing the isolation.

Categories

Resources