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 // 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 // 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)) }