Spring Framework offers two ways to programmatically manage transactions - using:
TransactionTemplateorTransactionalOperator.The
TransactionManagerimplementation itself.
The Spring team usually recommends TransactionTemplate for software transaction management in imperative flows and TransactionalOperator for reactive code. The second approach is similar to using the UserTransaction API from JTA, although exception handling is less overloaded.
Using the TransactionTemplate
Template TransactionTemplate takes the same approach as other Spring templates, such as JdbcTemplate. It uses a callback approach (to free application code from having to do the stereotypical acquisition and release of transactional resources) and entails creating code that is intent-driven, i.e. your code's work is focused solely on what you need to accomplish.
TransactionTemplate is completely binding you to the transaction infrastructure and Spring API. Whether programmatic transaction management is appropriate for your development needs is a matter for you to decide for yourself.
Application code that should run in a transactional context and that explicitly uses TransactionTemplate is similar to the following example. You, as an application developer, can write a TransactionCallback implementation (usually expressed as an anonymous inner class) containing the code to be executed in the context of a transaction. You can then pass an instance of your custom TransactionCallback to the execute(..) method exposed to the TransactionTemplate. The following example shows how to do this:
public class SimpleService implements Service {
// a single TransactionTemplate, common to all methods of this instance
private final TransactionTemplate transactionTemplate;
// use dependency injection via the constructor to provide a PlatformTransactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
// the code in this method is executed in the context of a transaction
public Object doInTransaction(TransactionStatus status) {
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
// use dependency injection via constructor to provide PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
// a single TransactionTemplate, common to all methods of this instance
private val transactionTemplate = TransactionTemplate(transactionManager)
fun someServiceMethod() = transactionTemplate.execute<Any?> {
updateOperation1()
resultOfUpdateOperation2()
}
}
If there is no return value, you can use a helper class TransactionCallbackWithoutResult with an anonymous class like this is shown below:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
updateOperation1()
updateOperation2()
}
})
The code inside the callback can roll back the transaction by calling the setRollbackOnly() method for the provided TransactionStatus object, as shown below:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessException ex) {
status.setRollbackOnly();
}
}
});
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
try {
updateOperation1()
updateOperation2()
} catch (ex: SomeBusinessException) {
status.setRollbackOnly()
}
}
})
Task transaction parameters
Transaction parameters (such as propagation mode, isolation level, timeout, and so on) can be set for the TransactionTemplate programmatically or in configuration. By default, TransactionTemplate instances have standard transaction settings. The following example shows programmatic configuration of transaction parameters for a specific TransactionTemplate:
public class SimpleService implements Service {
private final TransactionTemplate transactionTemplate;
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
// if desired, you can explicitly set the transaction parameters here
this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
this.transactionTemplate.setTimeout(30); // 30 seconds
// and so on...
}
}
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
private val transactionTemplate = TransactionTemplate(transactionManager).apply {
// if desired, you can explicitly set transaction parameters here
isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED timeout = 30 // 30 seconds
// and so on...
}
}
In the following example, TransactionTemplate is defined with some custom transaction settings using XML configuration in Spring:
<bean id="sharedTransactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
<property name="timeout" value="30"/>
</bean>
You can then inject sharedTransactionTemplate into as many services as needed.
Finally, instances of the TransactionTemplate class are thread-safe because instances do not retain any dialog state. However, TransactionTemplate instances retain configuration state. So, although multiple classes can share a single TransactionTemplate instance, if you want a class to use a TransactionTemplate with different settings (for example, different isolation levels), you must create two different ones instance of TransactionTemplate.
Using TransactionalOperator
TransactionalOperator follows the programming structure of the operator, which is similar to others reactive operators. It uses a callback approach (to free application code from having to do the stereotypical acquisition and release of transactional resources) and entails creating code that is intent-driven, i.e. your code's work is focused solely on what you need to accomplish.
TransactionalOperator is completely binding you to the transaction infrastructure and Spring API. Whether programmatic transaction management is suitable for your development needs is a matter for you to decide for yourself.
Application code that needs to be executed in the context of a transaction that explicitly uses the TransactionalOperator is similar to that which is represented in the following example:
public class SimpleService implements Service {
// a single TransactionalOperator, common to all methods of this instance
private final TransactionalOperator transactionalOperator;
// use constructor dependency injection to provide a ReactiveTransactionManager
public SimpleService(ReactiveTransactionManager transactionManager) {
this.transactionalOperator = TransactionalOperator.create(transactionManager);
}
public Mono<Object> someServiceMethod() {
// the code in this method is executed in the context of a transaction
Mono<Object> update = updateOperation1();
return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
}
}
// use dependency injection via constructor to provide ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
// a single TransactionalOperator, common to all methods of this instance
private val transactionalOperator = TransactionalOperator.create(transactionManager)
suspend fun someServiceMethod() = transactionalOperator.executeAndAwait<Any?> {
updateOperation1()
resultOfUpdateOperation2()
}
}
TransactionalOperator can be used in two ways:
Based on an operator using types from Project Reactor
(mono.as(transactionalOperator::transactional))Based on a callback for all other cases (
transactionalOperator.execute(TransactionCallback<T>))
The code inside the callback can roll back the transaction by calling the setRollbackOnly() method on the provided ReactiveTransaction object as shown below:
transactionalOperator.execute(new TransactionCallback<>() {
public Mono<Object> doInTransaction(ReactiveTransaction status) {
return updateOperation1().then(updateOperation2)
.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
}
}
});
transactionalOperator.execute(object : TransactionCallback() {
override fun doInTransactionWithoutResult(status: ReactiveTransaction) {
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, Cancellations may be made. It is impossible to find out the reason for cancellation, whether it was an error or simply a lack of interest in further consumption. Since version 5.3, cancellation signals result in a rollback. As a result, it is important to consider the operators used in descending order from Publisher transactions. Particularly in the case of Flux or other multi-valued Publisher, the full output must be consumed for the transaction to complete.
Setting transaction parameters
You can set transaction parameters (such as propagation mode, isolation level, timeout, etc.) for TransactionalOperator. By default, TransactionalOperator instances have standard transactional settings. The following example shows how to configure transactional parameters for a specific TransactionalOperator:
public class SimpleService implements Service {
private final TransactionalOperator transactionalOperator;
public SimpleService(ReactiveTransactionManager transactionManager) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// if desired, you can explicitly set the transaction parameters here
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
definition.setTimeout(30); // 30 seconds
// and so on...
this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
}
}
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
private val definition = DefaultTransactionDefinition().apply {
// if desired, you can explicitly set the transaction parameters here
isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED timeout = 30 // 30 seconds
// and so on...
}
private val transactionalOperator = TransactionalOperator(transactionManager, definition)
}
Using TransactionManager
The following sections cover programmatic use of imperative and reactive transaction managers.
Using PlatformTransactionManager
For imperative transactions, you can use org.springframework.transaction.PlatformTransactionManager to manage the transaction directly. To do this, pass the implementation of the PlatformTransactionManager you are using to your bean via a bean reference. Then, using the TransactionDefinition and TransactionStatus objects, transactions can be initiated, rolled back, and committed. The following example shows how to do this:
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicit setting of the transaction name can only be done 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);
val def = DefaultTransactionDefinition()
// explicitly setting the name transactions can only be done programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val status = txManager.getTransaction(def)
try {
// put your business logic here
} catch (ex: MyException) {
txManager.rollback (status)
throw ex
}
txManager.commit(status)
Using ReactiveTransactionManager
When working with With reactive transactions, you can use org.springframework.transaction.ReactiveTransactionManager to manage the transaction directly. To do this, pass the implementation of the ReactiveTransactionManager you are using to your bean via a bean reference. Then, using the TransactionDefinition and ReactiveTransaction objects, you can initiate, rollback, and commit transactions. The following example shows how to do this:
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicit setting of the transaction name can only be done 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)));
});
val def = DefaultTransactionDefinition()
// Explicit setting of the transaction name can only be done programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val reactiveTx = txManager.getReactiveTransaction(def)
reactiveTx.flatMap { status ->
val tx = ... // put your business logic here
tx.then(txManager.commit(status))
.onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) }
GO TO FULL VERSION