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.