Spring Framework offers two ways to programmatically manage transactions - using:
TransactionTemplate
orTransactionalOperator
.The
TransactionManager
implementation 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