Zademy

Springプログラマティックトランザクション管理:初心者向け実践ガイド

spring-boot; transactions
words 単語

Springの@Transactionalは宣言的トランザクション管理に非常に優れていますが、常に最適というわけではありません。この記事では、トランザクションのライフサイクルを細かく制御したい場面で使える、プログラマティックなトランザクション管理を整理します。

なぜプログラマティックトランザクションが必要か?

コネクションプール枯渇問題

データベース呼び出しと外部API呼び出しが混ざったメソッドを見てください。

@Transactional
public void processPayment(PaymentRequest request) {
    savePaymentRequest(request);              // DB
    callPaymentProviderApi(request);          // 外部API(遅い)
    updatePaymentState(request);              // DB
    saveAuditHistory(request);                // DB
}

何が起きるか。Springが@Transactionalでトランザクションを開始すると、メソッドの先頭でコネクションを1つ取得し、メソッドが終わるまで保持します。つまり、外部APIの応答を待っている間も、そのコネクションは占有されたままです。

API呼び出しに5〜10秒かかったとしましょう。その間、コネクションはただ待っているだけです。高負荷になると、全部のリクエストが同じようにコネクションを握り続けて、プールが枯渇します。私はこれで本番障害を起こしたことがあります。

同じトランザクション内でDB操作と外部API呼び出しを混ぜないこと。これが一番大切です。

解決策1:TransactionTemplate(推奨)

TransactionTemplateは、トランザクションを手動で管理するためのコールバックベースのAPIです。プログラマティックトランザクションを扱うなら、いちばんクリーンな方法だと思います。

基本的な設定

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.stereotype.Component;

@Component
public class PaymentService {
    
    private final TransactionTemplate transactionTemplate;
    
    public PaymentService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }
}

Spring BootはPlatformTransactionManagerを自動で設定してくれるので、インジェクションするだけで使えます。

例1:戻り値を持つトランザクション

public Long createSuccessfulPayment(PaymentRequest request) {
    // トランザクション内でコードを実行し、IDを返す
    Long paymentId = transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(request.getAmount());
        payment.setReferenceNumber(request.getReference());
        payment.setState(Payment.State.SUCCESSFUL);
        
        entityManager.persist(payment);
        
        // IDはpersist後に自動生成される
        return payment.getId();
    });
    
    return paymentId;
}

ラムダの中でトランザクションが始まり、正常に終わればコミット、例外が出ればロールバック、という流れが自動で回ります。戻り値も返せるので、persistしたエンティティのIDをそのまま受け取れます。

例2:例外時の自動ロールバック

public void createTwoPaymentsWithRollback() {
    try {
        transactionTemplate.execute(status -> {
            Payment first = new Payment();
            first.setReferenceNumber("REF-001");
            first.setAmount(1000L);
            entityManager.persist(first);  // OK
            
            Payment second = new Payment();
            second.setReferenceNumber("REF-001"); // 重複!
            second.setAmount(2000L);
            entityManager.persist(second);  // 例外をスロー
            
            return null;
        });
    } catch (Exception e) {
        // 最初の支払いも巻き戻される - 原子性が保証される
        System.out.println("トランザクションがロールバックされました:" + e.getMessage());
    }
}

2件目のpersistで例外が出ると、1件目も巻き戻ります。原子性が保証されているのがわかると思います。

例3:明示的な手動ロールバック

public Long createPaymentWithValidation(PaymentRequest request) {
    return transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setReferenceNumber(request.getReference());
        payment.setAmount(request.getAmount());
        
        entityManager.persist(payment);
        
        // カスタムビジネス検証
        if (request.getAmount() > 100000) {
            // ロールバック用にマーク - トランザクションは巻き戻される
            status.setRollbackOnly();
            return null;  // またはカスタム例外をスロー
        }
        
        return payment.getId();
    });
}

例外を投げる以外に、status.setRollbackOnly()で明示的にロールバックを指定することもできます。金額の上限チェックなど、ビジネスルール違反を判定する際に便利です。

例4:戻り値のないトランザクション

public void saveAuditLogEntry(AuditEntry entry) {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            auditRepository.save(entry);
            // 何も返さず、操作を実行するだけ
        }
    });
}

例5:インスタンスごとのカスタム設定

用途ごとに異なる設定のTransactionTemplateをBeanとして定義しておけば、使い分けが簡単です。

@Component
public class TransactionConfig {
    
    @Bean
    public TransactionTemplate readOnlyTransactionTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setReadOnly(true);  // クエリ用の最適化
        return template;
    }
    
    @Bean
    public TransactionTemplate serializableTransactionTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
        template.setTimeout(30);  // 秒
        return template;
    }
    
    @Bean
    public TransactionTemplate requiresNewTransactionTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return template;
    }
}

利用可能な設定

プロパティ値説明
setIsolationLevel()ISOLATION_READ_UNCOMMITTEDリードアンコミッティッド分離レベル
ISOLATION_READ_COMMITTEDコミット済みデータのみ読み取り
ISOLATION_REPEATABLE_READ反復不能読み取りを防止
ISOLATION_SERIALIZABLE最大分離
setPropagationBehavior()PROPAGATION_REQUIRED既存を使用するか、新規作成
PROPAGATION_REQUIRES_NEW常に新しいトランザクションを作成
PROPAGATION_NESTEDネストされたトランザクション(セーブポイント)
setTimeout()秒(int)最大実行時間
setReadOnly()true/false読み取り専用の最適化

解決策2:PlatformTransactionManager(低レベル)

もっと細かく制御したい場合は、PlatformTransactionManagerを直接使います。実は@TransactionalもTransactionTemplateも、内部的にはこのAPIを呼んでいます。

例:完全な手動制御

import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Component
public class ManualTransactionService {
    
    private final PlatformTransactionManager transactionManager;
    
    public ManualTransactionService(PlatformTransactionManager txManager) {
        this.transactionManager = txManager;
    }
    
    public void processWithTotalControl() {
        // 1. トランザクション設定を定義
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
        definition.setTimeout(5);  // 5秒
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        
        // 2. トランザクションを開始
        TransactionStatus status = transactionManager.getTransaction(definition);
        
        try {
            // 3. ビジネス操作を実行
            Payment payment = new Payment();
            payment.setAmount(500L);
            payment.setReferenceNumber("MANUAL-001");
            entityManager.persist(payment);
            
            // 4. トランザクションをコミット
            transactionManager.commit(status);
            
        } catch (Exception ex) {
            // 5. エラー時にロールバック
            transactionManager.rollback(status);
            throw new RuntimeException("手動トランザクションでエラー", ex);
        }
    }
}

getTransactionで開始、commitかrollbackで明示的に終了する。try-catchも自分で書く必要があります。冗長ですが、その分だけ自由度は高いです。

比較:TransactionTemplate vs PlatformTransactionManager

側面TransactionTemplatePlatformTransactionManager
抽象化レベル高(コールバック)低(手動)
エラー処理自動手動(try-catch)
自動ロールバックはいいいえ(呼び出す必要がある)
結果的なコードよりクリーンより冗長
柔軟性コールバックによる制限完全な制御
推奨される用途ほとんどのケース細かい制御が必要な場合

正直なところ、PlatformTransactionManagerを直接使う必要が出てくるケースは稀です。1つのメソッド内で複数の独立したトランザクションを制御するとか、非標準のトランザクションシステムと統合するとか、そういう場面以外はTransactionTemplateで十分です。

実践的なケース:DBと外部APIの分離

最初の問題を振り返ります。

@Transactional
public void processOrder(Order order) {
    orderRepository.save(order);              // DB
    shippingApi.createShipment(order);        // 遅いAPI
    notificationService.notifyCustomer(order); // 遅いAPI
}

これをTransactionTemplateで書き直すとこうなります:

@Component
public class OrderService {
    
    private final TransactionTemplate txTemplate;
    private final OrderRepository orderRepository;
    private final ShippingApiService shippingApi;
    private final NotificationService notificationService;
    
    public void processOrderSafely(Order order) {
        // ステップ1:トランザクション内でのDB部分のみ
        Long orderId = txTemplate.execute(status -> {
            order.setStatus("PROCESSING");
            orderRepository.save(order);
            return order.getId();
        });
        // 接続はすでに解放されている!
        
        // ステップ2:外部API(接続を占有しない)
        String trackingNumber = shippingApi.createShipment(order);
        
        // ステップ3:別の外部API(接続を占有しない)
        notificationService.notifyCustomer(order);
        
        // ステップ4:最終ステータスの更新(新しい短いトランザクション)
        txTemplate.executeWithoutResult(status -> {
            Order updated = orderRepository.findById(orderId).orElseThrow();
            updated.setTrackingNumber(trackingNumber);
            updated.setStatus("SHIPPED");
        });
    }
}

ポイントは、DB操作だけを短いトランザクションで囲んで、外部API呼び出しの間はコネクションを解放しているところです。コネクションが占有される時間が数秒からミリ秒単位に短縮されます。

各アプローチを使用するタイミング

@Transactionalを使うのは、単純なCRUDで外部API呼び出しがない場合。条件付きのロールバック制御が不要で、コードをシンプルに保ちたいときはこれで十分です。

TransactionTemplateに切り替える目安は、DB操作と外部I/Oが混ざる場合、条件付きロジックでロールバックを制御したい場合、同じサービス内で異なるトランザクション設定が必要な場合です。

PlatformTransactionManagerを直接使うのは、ライフサイクルを完全に制御したい場合、1メソッドで複数の独立したトランザクションを作る場合、詳細なトランザクションロギングや監査が必要な場合です。

プログラマティックトランザクションは、@Transactionalでは表現できない制御を可能にします。特にDB操作を外部呼び出しから分離して、コネクションプールの枯渇を防ぐ場面では、TransactionTemplateが一番の味方になります。

目的は@Transactionalを置き換えることではありません。宣言的アプローチでは設計が窮屈になる場面を補完することです。


参考文献と追加リソース

公式ドキュメント

  • Spring Framework - Programmatic Transaction Management: Springチームによるプログラマティックトランザクション管理の完全ガイド。 docs.spring.io
  • Spring Data Access Documentation: データアクセスとトランザクションに関する公式ドキュメント。 docs.spring.io

推奨記事

  • Vlad Mihalcea - Spring Transaction and Connection Management: Springがデータベース接続とトランザクションをどのように処理するかについての深い分析。 vladmihalcea.com
  • Baeldung - Programmatic Transaction Management in Spring: TransactionTemplateとPlatformTransactionManagerの実用的なチュートリアル。 baeldung.com
  • Marco Behler - Spring Transaction Management @Transactional In-Depth: Springでのトランザクションの内部動作に関する詳細ガイド。 marcobehler.com

追加のベストプラクティス

  1. 接続プールでauto-commit=falseを設定する - レイジー接続取得を有効にする
  2. Hibernate使用時はhibernate.connection.provider_disables_autocommit=trueを設定する
  3. DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTIONを検討する - 接続の再利用を最大化
  4. サービス層を設計する - トランザクションメソッドは実行のできるだけ遅い段階で呼び出す