1. 概要

Springの@Transactionalアノテーションは、トランザクションの境界をマークするための優れた宣言型APIを提供します。

舞台裏では、 @Transactional アノテーションが出現するたびに定義されるトランザクションの作成と維持が、アスペクトによって処理されます。 This approach makes it easy to decouple our core business logic from cross-cutting concerns such as transaction management.

このチュートリアルでは、これが常に最良のアプローチであるとは限らないことがわかります。 We’ll explore what programmatic alternatives Spring provides, such as TransactionTemplate, and our reasons for using them.

2. 楽園でのトラブル

単純なサービスで2つの異なるタイプのI/Oを混合していると仮定しましょう。

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

Here we have a few database calls alongside a possibly expensive REST API call. At first glance, it might make sense to make the whole method transactional since we may want to use one EntityManager to perform the whole operation atomically.

However, if that external API takes longer than usual to respond for whatever reason, we may soon run out of database connections!

2.1. 現実の過酷な性質

initialPaymentメソッドを呼び出すと次のようになります。

  1. The transactional aspect creates a new EntityManager and starts a new transaction, so it borrows one Connection from the connection pool.
  2. After the first database call, it calls the external API while keeping the borrowed Connection.
  3. Finally, it uses that Connection to perform the remaining database calls.

If the API call responds very slowly for a while, this method would hog the borrowed Connection while waiting for the response.

Imagine that during this period we get a burst of calls to the initialPayment method. In that case, all Connections may wait for a response from the API call. そのため、データベース接続が不足する可能性があります—バックエンドサービスが遅いためです!

Mixing the database I/O with other types of I/O in a transactional context isn’t a great idea. So, the first solution for these sorts of problems is to separate these types of I/O altogether. If for whatever reason we can’t separate them, we can still use Spring APIs to manage transactions manually.

3. TransactionTemplateを使用する

TransactionTemplate provides a set of callback-based APIs to manage transactions manually. In order to use it, we should first initialize it with a PlatformTransactionManager.

We can set up this template using dependency injection:

// test annotations
class ManualTransactionIntegrationTest {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private TransactionTemplate transactionTemplate;

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

    // omitted
}

The PlatformTransactionManager helps the template to create, commit or roll back transactions.

Spring Bootを使用する場合、タイプ PlatformTransactionManager の適切なBeanが自動的に登録されるため、単に注入するだけで済みます。 それ以外の場合は、手動で PlatformTransactionManagerbeanを登録する必要があります。

3.1. サンプルドメインモデル

From now on, for the sake of demonstration, we’re going to use a simplified payment domain model.

この単純なドメインには、各支払いの詳細をカプセル化する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
    }
}

また、 Testcontainers ライブラリを使用して、各テストケースの前に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. Transactions With Results

The TransactionTemplate offers a method called execute, which can run any given block of code inside a transaction and then return some result:

@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();
}

Here we’re persisting a new Payment instance into the database and then returning its auto-generated id.

Similar to the declarative approach, the template can guarantee atomicity for us.

If one of the operations inside a transaction fails to complete, it rolls back all of them:

@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();
}

Since the second referenceNumber is a duplicate, the database rejects the second persist operation, causing the whole transaction to roll back. Therefore, the database does not contain any payments after the transaction.

It’s also possible to manually trigger a rollback by calling the setRollbackOnly() on TransactionStatus:

@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コールバッククラスを使用できます。

@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を使用していました。 ほとんどの場合、このデフォルトで十分ですが、構成設定を変更することは可能です。

Let’s set the transaction isolation level:

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

同様に、トランザクションの伝播動作を変更できます。

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

または、トランザクションのタイムアウトを秒単位で設定できます。

transactionTemplate.setTimeout(1000);

読み取り専用トランザクションの最適化の恩恵を受けることも可能です。

transactionTemplate.setReadOnly(true);

Once we create a TransactionTemplate with a configuration, all transactions will use that configuration to execute. So, if we need multiple configurations, we should create multiple template instances.

4. PlatformTransactionManagerを使用する

In addition to the TransactionTemplate, we can use an even lower-level API such as PlatformTransactionManager to manage transactions manually. 非常に興味深いことに、@TransactionalTransactionTemplateはどちらも、このAPIを使用してトランザクションを内部で管理しています。

4.1. トランザクションの構成

Before using this API, we should define how our transaction is going to look.

Let’s set a three-second timeout with the repeatable read transaction isolation level:

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

トランザクション定義は、TransactionTemplate構成に似ています。 However, we can use multiple definitions with just one 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. 結論

In this article, we first saw when we should choose programmatic transaction management over the declarative approach.

Then, by introducing two different APIs, we learned how to manually create, commit or roll back any given transaction.

いつものように、サンプルコードはGitHubから入手できます。