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

wayne
  • 598
  • 3
  • 15

1 Answers1

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.

Kayaman
  • 72,141
  • 5
  • 83
  • 121
  • 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 Apr 02 '20 at 07:49
  • @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. – Kayaman Apr 02 '20 at 07:55
  • 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? – wayne Apr 02 '20 at 07:58
  • Well that's neither normal, nor is it according to the spec. Which database are you using? – Kayaman Apr 02 '20 at 08:00
  • It is IBM DB2 database – wayne Apr 02 '20 at 08:00
  • See if [this helps](https://stackoverflow.com/questions/13395794/how-do-i-get-transaction-info-in-spring-whether-transaction-is-committed-or-roll). – Kayaman Apr 02 '20 at 08:02
  • thanks @Kayaman. I've updated it accordingly. sorry for bugging you with trivial questions, but do you happen to know a way to check if transaction used in methodA and methodB are 2 different transactions? I'm using `sessionFactory.getCurrentSession().getTransaction()` to check if the transaction is the same object – wayne Apr 02 '20 at 09:20
  • Well the API doesn't provide a way to distinguish between transactions, but I'd expect them to be distinct objects for separate transactions, and the same object if it's running in the same one. Perhaps you can get the database logs to see what's happening on the other end? – Kayaman Apr 02 '20 at 09:33
  • my testing is proving that REQUIRES_NEW is not needed for methodB in order to not let methodB's RuntimeException roll back methodA. A sort of reasonable explanation is that rollback is done at transaction boundary and in the case where methodB does not have REQUIRES_NEW, the boundary is at the end of methodA. So the RuntimeException of methodB never reaches the boundary as it's caught inside methodA. Therefore the transaction is not marked to roll back. Do you think this makes sense? – wayne Apr 03 '20 at 07:12
  • @wayne ah I think we've found our misunderstanding. If you don't have **any** transaction annotation for `methodB()` and catch the exception then the transaction isn't rolled back. But you're comparing **nothing** vs. `REQUIRES_NEW`, when you should be comparing `REQUIRED` vs. `REQUIRES_NEW`. You're correct about the boundaries, but you didn't mention that in one case you have 1 boundary and in the other case you have 2. It changes the functionality quite a bit. – Kayaman Apr 03 '20 at 09:13
  • so given my code, whether methodB has no annotation / REQUIRED / REQUIRES_NEW will give me the same result when methodB has exception - that methodA can correctly commit? – wayne Apr 03 '20 at 10:47
  • @wayne that's not what I said. My answer still stands, it's the spec. If you're experiencing different behaviour, then there's a bug. I'm more interested about whether a non-database oriented `RuntimeException` would cause a rollback if caught. I.e. as the transaction isn't actually in trouble, would a `throw new RuntimeException();` allow the transaction to commit if `methodB()` doesn't have a `@Transactional` annotation (even though it still participates implicitly in methodA's transaction). Then again that would be broken code by any standards. – Kayaman Apr 03 '20 at 11:19
  • @wayne see [here](https://stackoverflow.com/questions/6097012/no-rollback-only-for-transaction-when-exception-occurs-in-submethod) for an example on how it works. Apparently you get an `UnexpectedRollbackException` if you catch the exception and pretend like nothing happened. Or maybe it's an extremely weird DB2 quirk, but I'd expect them to respect the spec. – Kayaman Apr 03 '20 at 11:23
  • Thanks @Kayaman I got you now (finally). You're right that the behavior I saw is because methodB doesn't have **any** transaction annotation (I've added why did this happen into my original post). As the result, there's only 1 transaction boundary and the RuntimeException never reaches it. To sum it up, in the case where `no transaction annotation` is there -> 1 boundary and methodA will not roll back; `REQUIRED` -> 2 boundaries, A will roll back; `REQUIRES_NEW` -> 2 boundaries, methodA will not roll back. Is my understanding correct? – wayne Apr 03 '20 at 22:54
  • @wayne yes, except it would depend on the type of the exception. If it's a `NullPointerException`, it would commit, if it's actually a database related exception, you would get the `UnexpectedRollbackException` (at least if I understood correctly). But what you had was broken code due to falling in the "calling from the same class", although in this case with `super.methodB()`. I'd recommend keeping your db code simple and with a flat inheritance hierarchy, no "clever stuff". It's hard enough to get it right as it is. – Kayaman Apr 04 '20 at 05:17