1

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

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

15 Comments

Thanks @Kayaman. This is also my understanding, but my testing result does not prove it that way. That's why I posted the question. Maybe I missed sth in my code, will check again
@wayne if you're not getting that behaviour in your code, then it's possible that the @Transactional annotation isn't being taken into account when calling methodB(). A trivial example would be doing new ClassB().methodB();, so that the method call isn't intercepted and the annotation isn't processed.
Thanks @Kayaman. I'm aware of that. The strange behavior I'm seeing is that methodA is committed correctly even if methodB does not have requires_new attribute. Just wondering if there's a way to check if current transaction is marked to be rollback?
Well that's neither normal, nor is it according to the spec. Which database are you using?
It is IBM DB2 database
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.