6
votes

It seems that when a transactional spring method with propagation of NESTED calls another transactional method with propagation REQUIRED, the inner transaction can force the rollback of the outer logical transaction.

For example, if the transactional method ClassA.methodA (which has propagation = REQUIRED) calls the transactional method ClassB.methodB (which has propagation = NESTED) which in turn calls transactional method ClassC.methodC (which has propagation = REQUIRED) then if ClassC.methodC initiates a rollback, then the outer logical transaction begun by ClassA.methodA will get an UnexpectedRollbackException if it attempts to commit.

It makes sense that the logical nested transaction owned by B should be forced to be rolled back if C fails, but it doesn't make sense that the logical transaction owned by C, and called from inside B which by itself can't affect A's logical transaction, should be able to affect A's logical transaction. It seems to create a situation in which methodA cannot treat methodB as a black box, since on the face of it, it seems that method B shouldn't be able to impact A's transaction, but depending on what it does internally, it may be able to violate the nested transaction boundary. Why is this possible? Why did spring choose to implement it this way?

Here's the code example discussed above:

public class ClassA {
  private ClassB classB;
  @Transactional(propagation = Propagation.REQUIRED)
  public void methodA() {
    try {
      classB.methodB();
     } catch (RuntimeException re) {
       // do nothing we don't want to fail, but alas we will get an UnexpectedRollbackException
     }
  }
}

public class ClassB {
  private ClassC classC;
  @Transactional(propagation = Propagation.NESTED)
  public void methodB() {
      classC.methodC();
  }
}

public class ClassC {
  @Transactional(propagation = Propagation.REQUIRED)
  public void methodC() {
      // If this method weren't transactional this would just return to the save point created by method B
      // I Want to just fail the NESTED transaction, but alas I'm going to fail everything
      throw new RuntimeException("Oh, woe is me!");
  }
}
1
This is 4 years old.. have you found a solution?Amit Goldstein

1 Answers

0
votes

I'm not sure why it works like that, but I found a workaround for this:

    public class ClassA {
        private ClassB classB;

        public void methodA() {
            try {
                classB.methodB();
            } catch (RuntimeException re) {
                // do nothing we don't want to fail, but alas we will get an UnexpectedRollbackException
            }
        }
    }

    public class ClassB {
        private ClassC classC;

        @Transactional(propagation = Propagation.NESTED)
        public void methodB() {
            classC.methodC();
        }
    }

    public class ClassC {
        private TransactionTemplate requiredTransaction = createRequiredTransactionTemplate();

        public void methodC() {
            if (TransactionSynchronizationManager.isActualTransactionActive()) {
                // We are already in transaction, do not wrap in transaction
                _methodC();
            } else {
                requiredTransaction.execute(cb -> {
                    _methodC();
                    return null;
                });
            }
        }

        private void _methodC() {
            throw new RuntimeException("Oh, woe is me!");
        }
    }