mcpField
T
transactional-patterns

transactional-patterns

github.com/rrezartprebreza開発328138 回インストール6 日前に更新
リポジトリ

transactional-patterns について

transactional-patterns は、アプリケーションコードで Spring トランザクションを扱うためのスキルです。既存の設計を保ちながら、伝播、ロールバック、並行処理、分散ワークフローに対応するコーディングエージェントを支援します。

主な機能

  1. 01トランザクション境界の選定
  2. 02伝播動作の理解
  3. 03ロールバックと例外の処理
  4. 04楽観ロックと再試行の適用
  5. 05Saga による分散ステップの調整
  6. 06コミット後の副作用と検証の管理

活用シーン

  1. 01サービス変更前のトランザクション注釈の確認
  2. 02検査例外のロールバック規則の実装
  3. 03新しいトランザクションでの楽観ロック競合への対応
  4. 04コミット後イベントの永続配信の計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill transactional-patterns

インストール後、Claude Code で/transactional-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
bad-transactional-service.java
good-transactional-service.java
templates/
TransactionalOrderService.java

詳細

トランザクションパターン

境界を明確にする

  • 伝播設定を変更する前に、プロジェクトのBoot/Frameworkのバージョン、トランザクションマネージャー、永続化技術、ロールバック設定、呼び出し元を確認します。
  • 複数ステップから成るビジネストランザクションは、アプリケーション/サービス境界に配置します。Spring Dataリポジトリのトランザクションは維持してください。リポジトリのアノテーションが本質的に誤りというわけではありません。
  • デフォルトの伝播設定はREQUIREDです。既存のトランザクションに参加するか、新しいトランザクションを作成します。
  • 適切な読み取り単位の処理にはreadOnly = trueを使用します。これは最適化のヒントであり、書き込みの禁止、認可、読み取りレプリカへの自動ルーティングを意味しません。
  • 既存のアーキテクチャとトランザクションマネージャーの選択を維持します。ローカルデータベースのトランザクションによって、別のデータベースやHTTPプロバイダーへの書き込みがアトミックになるわけではありません。

以下のスニペットは説明用です。ドメインクラスとインポートはアプリケーション側で用意します。 サービスを編集する際は、サービスのテンプレートを適用し、適切な例と不適切な例を比較してください。

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // default for all methods in this service
public class OrderService {

    @Transactional // overrides readOnly for writes
    public Order createOrder(CreateOrderRequest request) {
        inventoryService.reserve(request.items()); // participates in same TX
        return orderRepository.save(Order.from(request));
    }

    public Optional<Order> findById(UUID id) {
        return orderRepository.findById(id); // readOnly = true inherited
    }
}

伝播設定

伝播設定動作
REQUIRED(デフォルト)既存のトランザクションに参加するか、新しいトランザクションを作成します
REQUIRES_NEW常に新しいトランザクションを作成し、既存のトランザクションを一時停止します
SUPPORTSトランザクションがあれば参加し、なければトランザクションなしで処理します
NOT_SUPPORTED常にトランザクションなしで実行します
MANDATORY既存のトランザクションが必須です。ない場合は例外をスローします
NEVERトランザクションがあってはなりません。存在する場合は例外をスローします

参加するREQUIRED呼び出しでは、外側のトランザクションの分離レベル、タイムアウト、読み取り専用設定が使用されます。内側の処理で発生した失敗を捕捉しても、ロールバック専用マーカーは解除されません。外側のコミット時にもUnexpectedRollbackExceptionがスローされる可能性があります。

REQUIRES_NEWでは、外側のトランザクションがリソースを保持している間、別の接続が必要になります。接続プールの容量とロック競合を考慮してください。独立したコミットは、失敗した操作後も保持する必要がある記録(たとえば、試行された操作)のために確保し、ロールバックされた注文が作成されたと主張する成功監査レコードには使用しないでください。独立した監査トランザクションから、未コミットの親レコードを参照してはいけません。

// REQUIRES_NEW — for audit logging that must survive rollback
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(AuditEvent event) {
    auditRepository.save(event); // commits independently of parent TX
}

// Illustrative: record an attempt for an existing order; failures are unchecked.
@Transactional
public void processOrder(Order order) {
    auditService.logAuditEvent(new AuditEvent("ORDER_START", order.getId()));
    try {
        // ... process, may throw
    } catch (RuntimeException e) {
        auditService.logAuditEvent(new AuditEvent("ORDER_FAILED", order.getId()));
        throw e; // parent TX rolls back, audit TX already committed
    }
}

自己呼び出しの落とし穴

// ❌ BROKEN — self-invocation bypasses Spring proxy, @Transactional ignored
@Service
public class OrderService {
    @Transactional
    public void processAll(List<UUID> ids) {
        ids.forEach(id -> this.processSingle(id)); // bypasses proxy!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processSingle(UUID id) { ... } // never creates new TX
}

// ✅ FIX — extract the independently transactional operation to another bean
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderProcessor orderProcessor; // separate bean

    @Transactional
    public void processAll(List<UUID> ids) {
        ids.forEach(id -> orderProcessor.processSingle(id)); // goes through proxy
    }
}

例外の処理

// Default: RuntimeException and Error roll back; checked exceptions do not.
// Check project-wide rollback configuration before adding per-method rules.

@Transactional(rollbackFor = InsufficientInventoryException.class) // checked exception
public Order createOrder(CreateOrderRequest request) throws InsufficientInventoryException {
    ...
}

OptimisticLockExceptionから回復するために、noRollbackForを決して使用しないでください。永続化プロバイダーはトランザクションをロールバック専用としてマークします。ロールバックして、上限のあるポリシーと新しく取得したエンティティを使い、処理単位全体を新しいトランザクションで再試行してください。

楽観的ロック

@Entity
public class Order {
    @Version
    private Long version; // Hibernate handles conflicts automatically
}

// Handles concurrent updates
@Transactional
public Order updateStatus(UUID id, OrderStatus newStatus) {
    Order order = orderRepository.findById(id).orElseThrow();
    order.updateStatus(newStatus); // conflict can surface at flush or commit
    return orderRepository.save(order);
}

依存関係とアノテーションのインポートについては、バージョン別の再試行ガイダンスを使用してください。再試行を行うファサードがすでにトランザクションを保持していると、各試行のREQUIREDメソッドがそのトランザクションに参加してしまうため、注意してください。

分散トランザクション(Sagaパターン)

Sagaは永続化されたローカルステップを調整する仕組みです。リモート呼び出しを@Transactionalで囲んでも、Sagaは実装されません。

  1. 保留中の注文、Sagaの状態、送信コマンドを1つのローカルトランザクションでコミットします。
  2. 安定したプロバイダーの冪等性キーを使用し、そのトランザクションの外でコマンドをディスパッチします。
  3. 確認された各結果と次のコマンドを、別のローカルトランザクションでアトミックに永続化します。
  4. 拒否された場合は、失敗状態と補償コマンドを永続化します。補償は独立して再試行し、リモート側の結果が不明な場合はタイムアウトを調整します。

失敗状態を保存した後、同じトランザクション内でロールバックを引き起こす例外をスローしないでください。状態が消去されてしまいます。クラッシュ後の復旧は、永続化された状態から再開できる必要があります。プロジェクトで既に使われているワークフローの仕組みを使用してください。配送と重複による影響については、メッセージング/アウトボックスおよび冪等性を参照してください。

コミット後の副作用

ベストエフォートのローカル通知では、コミットに紐づくリスナーを使用することで、ロールバック時の配信を防げます。 確実な配信が必要な場合は、業務上の変更とともにアウトボックスエントリーを永続化するか、プロジェクトの永続的なパブリケーション機構を使用してください。AFTER_COMMITだけでは、プロセスのクラッシュから復旧したり、配信の失敗を再試行したりできません。

// Publisher — inside the TX
@Transactional
public Order place(UUID id) {
    Order order = orderRepository.findById(id).orElseThrow();
    order.place();
    eventPublisher.publishEvent(new OrderPlaced(order.getId())); // published now; listener defers handling
    return orderRepository.save(order);
}

// Listener — runs ONLY if the TX commits successfully
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlaced event) {
    emailService.sendConfirmation(event.orderId()); // best effort; no durable retry
}

AFTER_COMMITはDBのコミット後に実行されますが、リソースは完了したトランザクションに紐づいたままの場合があります。データベースへの書き込みは、別のREQUIRES_NEW Beanに委譲してください。リスナーが失敗しても、コミット済みの業務上の書き込みは取り消せません。配信の失敗を把握でき、復旧可能にしてください。 アクティブなトランザクションがない場合、デフォルトではリスナーはスキップされます。@Asyncを使用しても、配信は永続化されません。ドメインイベントを参照してください。

検証

Springプロキシ経由でテストし、新しいトランザクションからコミット済みデータを検証してください。チェック例外と非チェック例外の違い、内部でのロールバック専用伝播、コミット時の楽観的競合、監査のロールバック、コミット後とロールバック後のリスナーの動作を網羅してください。分離レベルと競合のテストには、本番環境と同じデータベースエンジンを使用してください。テスト全体のロールバックによって、コミット後の動作が隠れることがあります。

注意点

  • エージェントがリポジトリのトランザクションアノテーションを削除する — 作業単位と既存のSpring Dataの動作を維持してください。
  • エージェントがAFTER_COMMITを永続的なメッセージングとして扱う — 確実な配信が必要な場合は、アウトボックスまたは永続的なパブリケーションレジストリを使用してください。
  • エージェントがレプリカへのルーティングや書き込み保護にreadOnlyを使用する — フラグだけでは、どちらも実現されません。
  • エージェントがREQUIRES_NEWで成功監査エントリーをコミットする — 業務上の変更とともにコミットしてください。
  • エージェントが楽観的ロックによるロールバックを抑制する — 新しいトランザクションで再試行してください。
  • エージェントがthis上で@Transactionalメソッドを呼び出す — 自己呼び出しではプロキシを経由しません。
  • エージェントがチェック例外でロールバックすると想定する — rollbackForを追加する必要があります。
  • エージェントがprivateメソッドに@Transactionalを使用する — Springプロキシではインターセプトできません。

公式ソース

コメント