Рубрики
Без рубрики

Управление программными транзакциями весной

Узнайте, как программно управлять транзакциями весной и почему этот подход иногда лучше, чем просто использование декларативной аннотации транзакций.

Автор оригинала: Ali Dehghani.

1. Обзор

Аннотация @Transactional Spring предоставляет хороший декларативный API для обозначения границ транзакций.

За кулисами аспект заботится о создании и поддержании транзакций, как они определены в каждом вхождении аннотации @Transactional . Этот подход позволяет легко отделить нашу основную бизнес-логику от сквозных проблем , таких как управление транзакциями.

В этом уроке мы увидим, что это не всегда лучший подход. Мы рассмотрим, какие программные альтернативы предоставляет Spring , такие как TransactionTemplate , и наши причины их использования.

2. Неприятности в Раю

Предположим, мы смешиваем два разных типа ввода-вывода в простом сервисе:

@Transactional
public void initialPayment(PaymentRequest request) {
    savePaymentRequest(request); // DB
    callThePaymentProviderApi(request); // API
    updatePaymentState(request); // DB
    saveHistoryForAuditing(request); // DB
}

Здесь у нас есть несколько вызовов базы данных наряду с, возможно, дорогостоящим вызовом REST API. На первый взгляд, возможно, имеет смысл сделать весь метод транзакционным, поскольку мы можем использовать один EntityManager для выполнения всей операции атомарно.

Однако, если этот внешний API по какой-либо причине будет отвечать дольше, чем обычно, у нас скоро могут закончиться соединения с базой данных!

2.1. Суровая природа реальности

Вот что происходит, когда мы вызываем метод первоначального платежа :

  1. Транзакционный аспект создает новый EntityManager и запускает новую транзакцию – таким образом, он заимствует одно Соединение из пула соединений
  2. После первого вызова базы данных он вызывает внешний API, сохраняя при этом заимствованное Соединение
  3. Наконец, он использует это Соединение для выполнения оставшихся вызовов базы данных

Если вызов API отвечает очень медленно в течение некоторого времени, этот метод будет блокировать заимствованное соединение в ожидании ответа .

Представьте, что в течение этого периода мы получаем серию звонков по методу первоначальный платеж . Затем все Соединения могут ждать ответа от вызова API. Вот почему у нас могут закончиться соединения с базой данных — из-за медленной серверной службы!

Смешивание ввода-вывода базы данных с другими типами ввода-вывода в транзакционном контексте-это плохой запах. Итак, первое решение для такого рода проблем состоит в том, чтобы полностью разделить эти типы ввода-вывода/|. Если по какой-либо причине мы не можем разделить их, мы все равно можем использовать API Spring для управления транзакциями вручную.

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

TransactionTemplate предоставляет набор обратных вызовов на основе API для управления транзакциями вручную. Чтобы использовать его, во-первых, мы должны инициализировать его с помощью PlatformTransactionManager.

Например, мы можем настроить этот шаблон с помощью внедрения зависимостей:

// test annotations
class ManualTransactionIntegrationTest {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // omitted
}

PlatformTransactionManager помогает шаблону создавать, фиксировать или откатывать транзакции.

При использовании Spring Boot соответствующий компонент типа PlatformTransactionManager будет автоматически зарегистрирован, поэтому нам просто нужно просто ввести его. В противном случае мы должны вручную зарегистрировать a PlatformTransactionManager bean.

3.1. Пример модели Предметной области

Отныне, для демонстрации, мы будем использовать упрощенную модель платежного домена. В этом простом домене у нас есть объект Payment для инкапсуляции данных каждого платежа:

@Entity
public class Payment {

    @Id
    @GeneratedValue
    private Long id;

    private Long amount;

    @Column(unique = true)
    private String referenceNumber;

    @Enumerated(EnumType.STRING)
    private State state;

    // getters and setters

    public enum State {
        STARTED, FAILED, SUCCESSFUL
    }
}

Кроме того, мы будем запускать все тесты внутри тестового класса, используя библиотеку контейнеров тестов для запуска экземпляра PostgreSQL перед каждым тестовым случаем:

@DataJpaTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually
public class ManualTransactionIntegrationTest {

    @Autowired 
    private PlatformTransactionManager transactionManager;

    @Autowired 
    private EntityManager entityManager;

    @Container
    private static PostgreSQLContainer pg = initPostgres();

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    public void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // tests

    private static PostgreSQLContainer initPostgres() {
        PostgreSQLContainer pg = new PostgreSQLContainer<>("postgres:11.1")
                .withDatabaseName("baeldung")
                .withUsername("test")
                .withPassword("test");
        pg.setPortBindings(singletonList("54320:5432"));

        return pg;
    }
}

3.2. Операции с результатами

TransactionTemplate предлагает метод с именем execute , который может запускать любой заданный блок кода внутри транзакции, а затем возвращать некоторый результат:

@Test
void givenAPayment_WhenNotDuplicate_ThenShouldCommit() {
    Long id = transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(1000L);
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);

        return payment.getId();
    });

    Payment payment = entityManager.find(Payment.class, id);
    assertThat(payment).isNotNull();
}

Здесь мы сохраняем новый экземпляр Payment в базе данных, а затем возвращаем его автоматически сгенерированный идентификатор.

Подобно декларативному подходу, шаблон может гарантировать атомарность для нас. То есть, если одна из операций внутри транзакции не завершается, она откатывает все из них:

@Test
void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() {
    try {
        transactionTemplate.execute(status -> {
            Payment first = new Payment();
            first.setAmount(1000L);
            first.setReferenceNumber("Ref-1");
            first.setState(Payment.State.SUCCESSFUL);

            Payment second = new Payment();
            second.setAmount(2000L);
            second.setReferenceNumber("Ref-1"); // same reference number
            second.setState(Payment.State.SUCCESSFUL);

            entityManager.persist(first); // ok
            entityManager.persist(second); // fails

            return "Ref-1";
        });
    } catch (Exception ignored) {}

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}

Поскольку второй ссылочный номер является дубликатом, база данных отклоняет вторую операцию сохранения, что приводит к откату всей транзакции. Таким образом, база данных не содержит никаких платежей после транзакции. Также можно вручную вызвать откат, вызвав setRollbackOnly() on Статус транзакции :

@Test
void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() {
    transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(1000L);
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);
        status.setRollbackOnly();

        return payment.getId();
    });

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}

3.3. Сделки Без Результатов

Если мы не собираемся возвращать что-либо из транзакции, мы можем использовать класс TransactionCallbackWithoutResult callback:

@Test
void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            Payment payment = new Payment();
            payment.setReferenceNumber("Ref-1");
            payment.setState(Payment.State.SUCCESSFUL);

            entityManager.persist(payment);
        }
    });

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}

3.4. Пользовательские Конфигурации транзакций

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

Например, мы можем установить уровень изоляции транзакций :

transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);

Аналогично, мы можем изменить поведение распространения транзакций:

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Или мы можем установить тайм-аут в секундах для транзакции:

transactionTemplate.setTimeout(1000);

Можно даже извлечь выгоду из оптимизации транзакций только для чтения:

transactionTemplate.setReadOnly(true);

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

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

В дополнение к TransactionTemplate, мы можем использовать еще более низкоуровневый API, такой как PlatformTransactionManager для управления транзакциями вручную. Довольно интересно, что оба @Transactional и TransactionTemplate используют этот API для внутреннего управления своими транзакциями.

4.1. Настройка транзакций

Прежде чем использовать этот API, мы должны определить, как будет выглядеть наша транзакция. Например, мы можем установить трехсекундный тайм-аут с повторяемым уровнем изоляции транзакций чтения:

DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);

Определения транзакций аналогичны конфигурациям TransactionTemplate . Однако , мы можем использовать несколько определений только с одним PlatformTransactionManager .

4.2. Ведение Транзакций

После настройки нашей транзакции мы можем программно управлять транзакциями:

@Test
void givenAPayment_WhenUsingTxManager_ThenShouldCommit() {
 
    // transaction definition

    TransactionStatus status = transactionManager.getTransaction(definition);
    try {
        Payment payment = new Payment();
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);
        transactionManager.commit(status);
    } catch (Exception ex) {
        transactionManager.rollback(status);
    }

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}

5. Заключение

В этом учебном пособии, во-первых, мы увидели, когда следует выбирать программное управление транзакциями вместо декларативного подхода. Затем, представив два разных API, мы узнали, как вручную создавать, фиксировать или откатывать любую заданную транзакцию.

Как обычно, пример кода доступен на GitHub .