Zademy

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

spring-boot; transactions
words 単語

Springの@Transactionalアノテーションは宣言的なトランザクション管理に優れていますが、常に最良の選択ではありません。このJottingでは、トランザクションのライフサイクルを細かく管理する必要がある状況に最適な、Springでのプログラマティックなトランザクション制御をいつ、どのように使用するかを学びます。

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

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

この一般的なシナリオを想像してください:データベース呼び出しと外部APIを組み合わせたメソッド:

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

ここでの問題は何ですか?

Springが@Transactionalでトランザクションを作成すると:

  1. プールから接続を取得し、メソッド全体の間保持します
  2. 接続は占有されたまま、外部APIの応答を待ちます
  3. APIに5-10秒かかる場合、その間接続はブロックされます
  4. 高負荷下では、遅いAPIを待ってすべての利用可能な接続を枯渇させます

黄金律: 同じトランザクション内でデータベース操作と外部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;
}

これは何をしますか?

  • 自動的にトランザクションを作成
  • ラムダコードをトランザクション内で実行
  • すべてが順調に進めば、自動的にコミット
  • 例外が発生すれば、自動的にロールバック
  • ラムダからの値を返す

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

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

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

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

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

異なる設定を持つ複数のTransactionTemplateインスタンスを作成できます:

@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を直接使用します。これは、@TransactionalTransactionTemplateの両方が内部的に使用する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);
        }
    }
}

比較:TransactionTemplate vs PlatformTransactionManager

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

実践的なケース: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");
        });
    }
}

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

@Transactional(宣言的)を使用する場合:

  • 単純なCRUD操作
  • 外部API呼び出しがない
  • 複雑な条件付き制御が不要
  • コードをクリーンで読みやすく保ちたい

TransactionTemplate(プログラマティック)を使用する場合:

  • DB操作と外部I/Oを混在させる必要がある
  • 条件付きロジックがロールバックを決定する
  • 同じサービスで異なるトランザクション設定が必要
  • トランザクションから値を返す必要がある

PlatformTransactionManagerを使用する場合:

  • ライフサイクルの完全な制御が必要
  • 1つのメソッドで複数のトランザクションが必要
  • 非標準のトランザクションシステムとの統合
  • 詳細なトランザクションロギングまたは監査

結論

プログラマティックトランザクションは、@Transactionalが提供できない制御を提供します。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. サービス層を設計する - トランザクションメソッドは実行のできるだけ遅い段階で呼び出す