Spring Framework предлагает два способа программного управления транзакциями – с помощью:

  • TransactionTemplate или TransactionalOperator.

  • Непосредственно реализации TransactionManager.

Команда Spring обычно рекомендует TransactionTemplate для программного управления транзакциями в императивных потоках и TransactionalOperator для реактивного кода. Второй подход похож на использование API-интерфейса UserTransaction из JTA, хотя обработка исключений менее перегруженная.

Использование TransactionTemplate

Шаблон TransactionTemplate задействует тот же подход, что и другие шаблоны Spring, такие как JdbcTemplate. Он задействует подход обратного вызова (чтобы освободить код приложения от необходимости выполнять стереотипное получение и освобождение транзакционных ресурсов) и влечет за собой создание кода, который управляется намерением, т.е. ваш работа вашего кода сосредоточена исключительно на том, что вам нужно осуществить.

Как показывают следующие примеры, использование TransactionTemplate полностью привязывает вас к инфраструктуре транзакций и API-интерфейсу Spring. Подходит ли программное управление транзакциями для ваших потребностей разработки – это вы должны решать самостоятельно.

Код приложения, который должен выполняться в транзакционном контексте и который явным образом использует TransactionTemplate, похож на приведенный далее пример. Вы, как разработчик приложения, можете написать реализацию TransactionCallback (обычно выраженную как анонимный внутренний класс), содержащую код, который нужно выполнить в контексте транзакции. Затем вы можете передать экземпляр вашего кастомного TransactionCallback в метод execute(..), открытый для TransactionTemplate. В следующем примере показано, как это сделать:

Java
public class SimpleService implements Service {
    // единый TransactionTemplate, общий для всех методов данного экземпляра
    private final TransactionTemplate transactionTemplate;
    // используем внедрение зависимостей через конструктор для предоставления PlatformTransactionManager
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }
    public Object someServiceMethod() {
        return transactionTemplate.execute(new TransactionCallback() {
            // код в этом методе выполняется в контексте транзакции
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}
Kotlin
// используем внедрение зависимостей через конструктор для предоставления PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
    // единый TransactionTemplate, общий для всех методов данного экземпляра
    private val transactionTemplate = TransactionTemplate(transactionManager)
    fun someServiceMethod() = transactionTemplate.execute<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

Если возвращаемого значения нет, можно использовать вспомогательный класс TransactionCallbackWithoutResult с анонимным классом, как это показано ниже:

Java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});
Kotlin
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        updateOperation1()
        updateOperation2()
    }
})

Код внутри обратного вызова может откатить транзакцию назад, вызвав метод setRollbackOnly() для предоставленного объекта TransactionStatus, как показано ниже:

Java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});
Kotlin
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        try {
            updateOperation1()
            updateOperation2()
        } catch (ex: SomeBusinessException) {
            status.setRollbackOnly()
        }
    }
})

Задание параметров транзакции

Параметры транзакции (такие как режим распространения, уровень изоляции, время ожидания и так далее) можно задать для шаблоне TransactionTemplate программно или в конфигурации. По умолчанию экземпляры TransactionTemplate имеют стандартные настройки транзакций. В следующем примере показана программная настройка параметров транзакций для конкретного TransactionTemplate:

Java
public class SimpleService implements Service {
    private final TransactionTemplate transactionTemplate;
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        // при желании здесь можно явным образом задать параметры транзакции
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 секунд
        // и так далее...
    }
}
Kotlin
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
    private val transactionTemplate = TransactionTemplate(transactionManager).apply {
        // при желании здесь можно явным образом задать параметры транзакции
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
        timeout = 30 // 30 секунд
        // и так далее...
    }
}

В следующем примере TransactionTemplate определен с некоторыми кастомными настройками транзакций с помощью XML-конфигурации в Spring:

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

Затем можно внедрить sharedTransactionTemplate в такое количество служб, какое потребуется.

Наконец, экземпляры класса TransactionTemplate являются потокобезопасными, поскольку экземпляры не сохраняют никакого диалогового состояния. Однако экземпляры TransactionTemplate сохраняют конфигурационное состояние. Таким образом, хотя несколько классов могут совместно использовать один экземпляр TransactionTemplate, если нужно, чтобы класс использовал TransactionTemplate с различными параметрами (например, с различным уровнем изоляции), необходимо создать два разных экземпляра TransactionTemplate.

Использование TransactionalOperator

TransactionalOperator следует программной структуре оператора, который схож с другими реактивными операторами. Он задействует подход обратного вызова (чтобы освободить код приложения от необходимости выполнять стереотипное получение и освобождение транзакционных ресурсов) и влечет за собой создание кода, который управляется намерением, т.е. ваш работа вашего кода сосредоточена исключительно на том, что вам нужно осуществить.

Как показывают следующие примеры, использование TransactionalOperator полностью привязывает вас к инфраструктуре транзакций и API-интерфейсу Spring. Подходит ли программное управление транзакциями для ваших потребностей разработки – это вы должны решать самостоятельно.

Код приложения, который нужно выполнить в контексте транзакции и который явным образом задействует TransactionalOperator, похож на тот, что представлен в следующем примере:

Java
public class SimpleService implements Service {
    // единый TransactionalOperator, общий для всех методов данного экземпляра
    private final TransactionalOperator transactionalOperator;
    // используем внедрение зависимостей через конструктор для предоставления ReactiveTransactionManager
    public SimpleService(ReactiveTransactionManager transactionManager) {
        this.transactionalOperator = TransactionalOperator.create(transactionManager);
    }
    public Mono<Object> someServiceMethod() {
        // код в этом методе выполняется в контексте транзакции
        Mono<Object> update = updateOperation1();
        return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
    }
}
Kotlin
// используем внедрение зависимостей через конструктор для предоставления ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
    // единый TransactionalOperator, общий для всех методов данного экземпляра
    private val transactionalOperator = TransactionalOperator.create(transactionManager)
    suspend fun someServiceMethod() = transactionalOperator.executeAndAwait<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

TransactionalOperator можно использовать двумя способами:

  • На основе оператора, использующего типы из Project Reactor (mono.as(transactionalOperator::transactional))

  • На основе обратного вызова для всех остальных случаев (transactionalOperator.execute(TransactionCallback<T>))

Код внутри обратного вызова может откатить транзакцию назад, вызвав метод setRollbackOnly() для предоставленного объекта ReactiveTransaction, как показано ниже:

Java
transactionalOperator.execute(new TransactionCallback<>() {
    public Mono<Object> doInTransaction(ReactiveTransaction status) {
        return updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
        }
    }
});
Kotlin
transactionalOperator.execute(object : TransactionCallback() {
    override fun doInTransactionWithoutResult(status: ReactiveTransaction) {
        updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly())
    }
})

Сигналы отмены

В Reactive Streams Subscriber может отменить свою Subscription и остановить своего Publisher. Операторы в Project Reactor, а также в других библиотеках, такие как next(), take(long), timeout(Duration) и другие, могут осуществлять отмены. Узнать причину отмены, будь то ошибка или просто отсутствие интереса к дальнейшему потреблению, невозможно. Начиная с версии 5.3 сигналы отмены приводят к откату. В результате важно рассмотреть операторов, используемых в нисходящем от Publisher транзакций порядке. В частности, в случае Flux или другого многозначного Publisher, для завершения транзакции должен быть потреблен полный вывод.

Задание параметров транзакции

Вы можете задавать параметры транзакции (такие как режим распространения, уровень изоляции, время ожидания и т.д.) для TransactionalOperator. По умолчанию экземпляры TransactionalOperator имеют стандартные транзакционные параметры. В следующем примере показана настройка транзакционных параметров для конкретного TransactionalOperator:

Java
public class SimpleService implements Service {
    private final TransactionalOperator transactionalOperator;
    public SimpleService(ReactiveTransactionManager transactionManager) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        // при желании здесь можно явным образом задать параметры транзакции
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        definition.setTimeout(30); // 30 секунд
        // и так далее...
        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}
Kotlin
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
    private val definition = DefaultTransactionDefinition().apply {
        // при желании здесь можно явным образом задать параметры транзакции
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
        timeout = 30 // 30 секунд
        // и так далее...
    }
    private val transactionalOperator = TransactionalOperator(transactionManager, definition)
}

Использование TransactionManager

В следующих разделах рассказывается о программном использовании императивных и реактивных диспетчеров транзакций.

Использование PlatformTransactionManager

Для императивных транзакций можно использовать org.springframework.transaction.PlatformTransactionManager непосредственно для управления транзакцией. Для этого передайте реализацию используемого вами PlatformTransactionManager в ваш бин через ссылку на бин. Затем, используя объекты TransactionDefinition и TransactionStatus, можно инициировать транзакции, откатывать и фиксировать. В следующем примере показано, как это сделать:

Java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// явную установку имени транзакции можно сделать только программно
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
    // помещаем сюда вашу бизнес-логику
} catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
Kotlin
val def = DefaultTransactionDefinition()
// явную установку имени транзакции можно сделать только программно
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val status = txManager.getTransaction(def)
try {
    // помещаем сюда вашу бизнес-логику
} catch (ex: MyException) {
    txManager.rollback(status)
    throw ex
}
txManager.commit(status)

Использование ReactiveTransactionManager

При работе с реактивными транзакциями можно использовать org.springframework.transaction.ReactiveTransactionManager непосредственно для управления транзакцией. Для этого передайте реализацию используемого вами ReactiveTransactionManager в ваш бин через ссылку на бин. Затем, используя объекты TransactionDefinition и ReactiveTransaction, можно инициировать транзакции, откатывать и фиксировать. В следующем примере показано, как это сделать:

Java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// явную установку имени транзакции можно сделать только программно
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);
reactiveTx.flatMap(status -> {
    Mono<Object> tx = ...; // помещаем сюда вашу бизнес-логику
    return tx.then(txManager.commit(status))
            .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});
Kotlin
val def = DefaultTransactionDefinition()
// явную установку имени транзакции можно сделать только программно
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val reactiveTx = txManager.getReactiveTransaction(def)
reactiveTx.flatMap { status ->
    val tx = ... // помещаем сюда вашу бизнес-логику
    tx.then(txManager.commit(status))
            .onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) }
}