Programmatic Transaction Management

The TODAY Framework provides two means of programmatic transaction management, by using:

  • The TransactionTemplate or TransactionalOperator.

  • A TransactionManager implementation directly.

The Infra team generally recommends the TransactionTemplate for programmatic transaction management in imperative flows and TransactionalOperator for reactive code. The second approach is similar to using the JTA UserTransaction API, although exception handling is less cumbersome.

Using the TransactionTemplate

The TransactionTemplate adopts the same approach as other Infra templates, such as the JdbcTemplate. It uses a callback approach (to free application code from having to do the boilerplate acquisition and release transactional resources) and results in code that is intention driven, in that your code focuses solely on what you want to do.

As the examples that follow show, using the TransactionTemplate absolutely couples you to Infra transaction infrastructure and APIs. Whether or not programmatic transaction management is suitable for your development needs is a decision that you have to make yourself.

Application code that must run in a transactional context and that explicitly uses the TransactionTemplate resembles the next example. You, as an application developer, can write a TransactionCallback implementation (typically expressed as an anonymous inner class) that contains the code that you need to run in the context of a transaction. You can then pass an instance of your custom TransactionCallback to the execute(..) method exposed on the TransactionTemplate. The following example shows how to do so:

  • Java

public class SimpleService implements Service {

  // single TransactionTemplate shared amongst all methods in this instance
  private final TransactionTemplate transactionTemplate;

  // use constructor-injection to supply the PlatformTransactionManager
  public SimpleService(PlatformTransactionManager transactionManager) {
    this.transactionTemplate = new TransactionTemplate(transactionManager);
  }

  public Object someServiceMethod() {
    return transactionTemplate.execute(new TransactionCallback() {
      // the code in this method runs in a transactional context
      public Object doInTransaction(TransactionStatus status) {
        updateOperation1();
        return resultOfUpdateOperation2();
      }
    });
  }
}

If there is no return value, you can use the convenient TransactionCallbackWithoutResult class with an anonymous class, as follows:

  • Java

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
  protected void doInTransactionWithoutResult(TransactionStatus status) {
    updateOperation1();
    updateOperation2();
  }
});

Code within the callback can roll the transaction back by calling the setRollbackOnly() method on the supplied TransactionStatus object, as follows:

  • Java

transactionTemplate.execute(new TransactionCallbackWithoutResult() {

  protected void doInTransactionWithoutResult(TransactionStatus status) {
    try {
      updateOperation1();
      updateOperation2();
    } catch (SomeBusinessException ex) {
      status.setRollbackOnly();
    }
  }
});

Specifying Transaction Settings

You can specify transaction settings (such as the propagation mode, the isolation level, the timeout, and so forth) on the TransactionTemplate either programmatically or in configuration. By default, TransactionTemplate instances have the default transactional settings. The following example shows the programmatic customization of the transactional settings for a specific TransactionTemplate:

  • Java

public class SimpleService implements Service {

  private final TransactionTemplate transactionTemplate;

  public SimpleService(PlatformTransactionManager transactionManager) {
    this.transactionTemplate = new TransactionTemplate(transactionManager);

    // the transaction settings can be set here explicitly if so desired
    this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
    this.transactionTemplate.setTimeout(30); // 30 seconds
    // and so forth...
  }
}

The following example defines a TransactionTemplate with some custom transactional settings by using Infra XML configuration:

<bean id="sharedTransactionTemplate"
    class="cn.taketoday.transaction.support.TransactionTemplate">
  <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
  <property name="timeout" value="30"/>
</bean>

You can then inject the sharedTransactionTemplate into as many services as are required.

Finally, instances of the TransactionTemplate class are thread-safe, in that instances do not maintain any conversational state. TransactionTemplate instances do, however, maintain configuration state. So, while a number of classes may share a single instance of a TransactionTemplate, if a class needs to use a TransactionTemplate with different settings (for example, a different isolation level), you need to create two distinct TransactionTemplate instances.

Using the TransactionalOperator

The TransactionalOperator follows an operator design that is similar to other reactive operators. It uses a callback approach (to free application code from having to do the boilerplate acquisition and release transactional resources) and results in code that is intention driven, in that your code focuses solely on what you want to do.

As the examples that follow show, using the TransactionalOperator absolutely couples you to Infra transaction infrastructure and APIs. Whether or not programmatic transaction management is suitable for your development needs is a decision that you have to make yourself.

Application code that must run in a transactional context and that explicitly uses the TransactionalOperator resembles the next example:

  • Java

public class SimpleService implements Service {

  // single TransactionalOperator shared amongst all methods in this instance
  private final TransactionalOperator transactionalOperator;

  // use constructor-injection to supply the ReactiveTransactionManager
  public SimpleService(ReactiveTransactionManager transactionManager) {
    this.transactionalOperator = TransactionalOperator.create(transactionManager);
  }

  public Mono<Object> someServiceMethod() {

    // the code in this method runs in a transactional context

    Mono<Object> update = updateOperation1();

    return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
  }
}

TransactionalOperator can be used in two ways:

  • Operator-style using Project Reactor types (mono.as(transactionalOperator::transactional))

  • Callback-style for every other case (transactionalOperator.execute(TransactionCallback<T>))

Code within the callback can roll the transaction back by calling the setRollbackOnly() method on the supplied ReactiveTransaction object, as follows:

  • Java

transactionalOperator.execute(new TransactionCallback<>() {

  public Mono<Object> doInTransaction(ReactiveTransaction status) {
    return updateOperation1().then(updateOperation2)
          .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
    }
  }
});

Cancel Signals

In Reactive Streams, a Subscriber can cancel its Subscription and stop its Publisher. Operators in Project Reactor, as well as in other libraries, such as next(), take(long), timeout(Duration), and others can issue cancellations. There is no way to know the reason for the cancellation, whether it is due to an error or a simply lack of interest to consume further. Since version 5.3 cancel signals lead to a roll back. As a result it is important to consider the operators used downstream from a transaction Publisher. In particular in the case of a Flux or other multi-value Publisher, the full output must be consumed to allow the transaction to complete.

Specifying Transaction Settings

You can specify transaction settings (such as the propagation mode, the isolation level, the timeout, and so forth) for the TransactionalOperator. By default, TransactionalOperator instances have default transactional settings. The following example shows customization of the transactional settings for a specific TransactionalOperator:

  • Java

public class SimpleService implements Service {

  private final TransactionalOperator transactionalOperator;

  public SimpleService(ReactiveTransactionManager transactionManager) {
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

    // the transaction settings can be set here explicitly if so desired
    definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
    definition.setTimeout(30); // 30 seconds
    // and so forth...

    this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
  }
}

Using the TransactionManager

The following sections explain programmatic usage of imperative and reactive transaction managers.

Using the PlatformTransactionManager

For imperative transactions, you can use a cn.taketoday.transaction.PlatformTransactionManager directly to manage your transaction. To do so, pass the implementation of the PlatformTransactionManager you use to your bean through a bean reference. Then, by using the TransactionDefinition and TransactionStatus objects, you can initiate transactions, roll back, and commit. The following example shows how to do so:

  • Java

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
  // put your business logic here
} catch (MyException ex) {
  txManager.rollback(status);
  throw ex;
}
txManager.commit(status);

Using the ReactiveTransactionManager

When working with reactive transactions, you can use a cn.taketoday.transaction.ReactiveTransactionManager directly to manage your transaction. To do so, pass the implementation of the ReactiveTransactionManager you use to your bean through a bean reference. Then, by using the TransactionDefinition and ReactiveTransaction objects, you can initiate transactions, roll back, and commit. The following example shows how to do so:

  • Java

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);

reactiveTx.flatMap(status -> {

  Mono<Object> tx = ...; // put your business logic here

  return tx.then(txManager.commit(status))
      .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});