0
votes

I have two methods in the JPA repository. Both the methods have propagation level as REQUIRED The methods are used to persist entity objects using Hibernate to Postgresql

@Transactional(propagation = Propagation.REQUIRED)
public void persistEmployee() {
    Employee employee = new Employee("Peter", "Washington DC");
    entityManager.persist(employee);
    try {
        persistLineManager();
    }
    catch( Exception e ) {
         // some task
    }
}

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class)
public void persistLineManager() {
    Employee lineManager = new Employee("John", "NYC");
    entityManager.persist(lineManager);
    if(lineManager != null) // intentionally! To trigger rollback
        throw new RuntimeException("Rollback!");
}

As per Spring docs when propagation level is REQUIRED both methods will run in the same transaction. In my code, I am intentionally throwing the Exception to trigger the rollback but still, both the entities are getting persisted. But I believe both the operations should be rollbacked. Please correct if my understanding is incorrect and let me know the correct way to rollback both the operations.

PROPAGATION_REQUIRES_NEW: [ from spring Docs ]

PROPAGATION_REQUIRES_NEW, in contrast to PROPAGATION_REQUIRED, uses a completely independent transaction for each affected transaction scope. In that case, the underlying physical transactions are different and hence can commit or roll back independently, with an outer transaction not affected by an inner transaction’s rollback status.

1

1 Answers

5
votes

PROXYFICATION

In your service, you created 2 methods, both @Transactional. When you create your bean, spring will create a proxy to add for you at runtime the behavior for a Transactional method. Let's dive deeper: Proxy by spring This proxyfication is illustrated by the image. Any caller form the outside world will not directly talk to you, but to your proxy. And then, the proxy will call you to execute the code of your service.

Now, this "Any caller form the outside world will not directly talk to you" is very important. If you make an inner call, like you do in persistEmployee which is calling persistLineManager, then you do not pass through the proxy. You call directly your method, NO PROXY. Therefor, the annotations at the top of your persistLineManager method are not read.

So, when persistLineManager is throwing a RuntimeException, the exception is catched directly by your caller persistEmployee, you go directly in your catch. As there is no proxy, there is no rollback because the transactional proxy did not catch the exception.

If you do only this, you will have a rollback occurring:

@Transactional(propagation = Propagation.REQUIRED)
public void persistEmployee() {
    Employee employee = new Employee("Peter", "Washington DC");
    entityManager.persist(employee);
    persistLineManager();
    // Don't catch and the exception will be catched by the transaction proxy, which will rollback
}

public void persistLineManager() {
    Employee lineManager = new Employee("John", "NYC");
    entityManager.persist(lineManager);
    if(lineManager != null) // intentionally! To trigger rollback
        throw new RuntimeException("Rollback!");
}

By default, @Transactional rollback for a RuntimeException

TRANSACTION TEMPLATE

Suppose you still want both method to be transactional independently, what you can do is using the TransactionTemplate. Here is an example:

class MyService {
    // Have a field of type TransactionTemplate
    private TransactionTemplate template;

    // In the constructor, Spring will inject the correct bean
    public MyService(PlatformTransactionManager transactionManager) {
        template = new TransactionTemplate(transactionManager);
        // Set this here if you always want this behaviour for your programmatic transaction
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }

    // Here you start your first transaction when arriving from the outside
    @Transactional(propagation = Propagation.REQUIRED)
    public void persistEmployee() {
        Employee employee = new Employee("Peter", "Washington DC");
        entityManager.persist(employee);
        // Inner call
        try {
            persistLineManager();
        } catch (RuntimeException e) {
            // Do what you want
        }
    }

    public void persistLineManager() {
        // Here, ask to your transactionTemplate to execute your code.
        template.execute(status -> {
            Employee lineManager = new Employee("John", "NYC");
            entityManager.persist(lineManager);
            if(lineManager != null) // intentionally! To trigger rollback
                throw new RuntimeException("Rollback!");
            return null;
        });
    }
}

I haven't tested everything, you might face some errors, but I hope you get the idea.

PROPAGATION

Let me add a last part about the difference between PROPAGATION_REQUIRED and PROPAGATION_REQUIRES_NEW:

PROPAGATION_REQUIRED: PROPAGATION_REQUIRED

  • Either I have no transaction, then I create one
  • Or There is a running transaction, and I join it.

PROPAGATION_REQUIRES: PROPAGATION_REQUIRES_NEW

  • In any situations, whether a transaction is running or not, I create a new one.

Example:

  • A client is entering in my transactional method, with PROPAGATION_REQUIRED. It creates a transaction name "TA".
  • This transactional method calls a method, which is also transactional, but PROPAGATION_REQUIRES_NEW. It creates a second transaction named "TB"
  • With have now: "client" -> "TA" -> "TB"
  • But the second method triggers a rollback. In that case, only "TB" will be rolledback, as "TA" and "TB" are 2 differents transactions.
  • So, in DB, I will persist every operation that has been made in "TA", but not in "TB".

Hope it helps