バックエンドフレームワーク間の宣言的なトランザクション管理
Min-jun Kim
Dev Intern · Leapcell

はじめに
バックエンド開発の複雑な世界では、データの整合性と信頼性を確保することが最優先事項です。たとえば、資金移動が一方のアカウントからの引き落としともう一方のアカウントへの入金を含む銀行アプリケーションを想像してみてください。システムが引き落とし後に、入金前 に失敗した場合、データ破損や金銭的損失を防ぐために、トランザクション全体をロールバックする必要があります。これがトランザクションが登場する場所であり、一連の操作に対して「すべて実行するか、すべて実行しないか」の保証を提供します。これらのトランザクションを手動で管理することは、退屈でエラーが発生しやすく、ビジネスロジック全体にトランザクション関連の定型コードを散在させる可能性があります。これに対処するために、最新のバックエンドフレームワークは宣言的なトランザクション管理を提供し、開発者は単純なアノテーションや構成でトランザクション境界を定義し、基盤となる複雑さを抽象化できるようにします。この記事では、3つの主要なバックエンドフレームワーク – Spring、ASP.NET Core、そして古くからあるEJB – が宣言的なトランザクション管理にどのようにアプローチし、実装しているかを掘り下げ、それらの類似点と相違点を明らかにします。
コアコンセプト
各フレームワークの詳細に入る前に、宣言的なトランザクション管理の理解に不可欠ないくつかのコアコンセプトを簡単に定義しましょう。
- トランザクション: 完全に完了するか(コミット)、まったく効果がないか(ロールバック)する単一の論理的な作業単位。ACID特性(原子性、一貫性、分離性、永続性)を遵守します。
- 宣言的なトランザクション管理: 明示的なプログラムによる呼び出しではなく、アノテーションまたはXML構成などを介して、ビジネスロジックの外部でトランザクション境界が定義されるプログラミングパラダイム。
- アスペクト指向プログラミング (AOP): 相互に関連する関心事(トランザクション管理、ロギング、セキュリティなど)をコアビジネスロジックから分離することを可能にすることで、モジュール性を向上させることを目的としたプログラミングパラダイム。多くの宣言的なトランザクション実装はAOPを活用しています。
- プロキシパターン: 実際のオブジェクトへのアクセスを制御したり、実際のオブジェクトのメソッドを呼び出す前後に(トランザクション管理などの)追加機能を追加したりするために、しばしばインターフェースを提供する構造的デザインパターン。
- トランザクションマネージャー/コーディネーター: 1つ以上のリソースが関与する操作の開始、コミット、ロールバックを含む、トランザクションをオーケストレーションする責任を負うコンポーネント。
- トランザクション属性/設定: プロパゲーション動作(例:
REQUIRED、REQUIRES_NEW)、分離レベル(例:READ_COMMITTED、SERIALIZABLE)、ロールバックルールなど、トランザクションの動作を指示する構成オプション。
宣言的なトランザクション管理の実装
@Transactional を使用したSpring Framework
Springの宣言的なトランザクション管理へのアプローチは、おそらく最も広く採用され、影響力のあるものの1つです。AOPを活用し、主にプロキシを介してメソッド呼び出しをインターセプトし、トランザクション動作を適用します。
原則と実装:
Springの@Transactionalアノテーションは、クラスまたはメソッドに配置できます。Spring管理Beanの@Transactionalアノテーションが付いたメソッドが呼び出されると、SpringはそのBeanの周りにプロキシを作成します。メソッド実行前に、プロキシはトランザクションを開始します。実行後、結果(例: 未処理の例外は通常ロールバックをトリガーします)に基づいてトランザクションをコミットまたはロールバックします。
Springはさまざまなトランザクションマネージャーをサポートしており、JDBC、JPA、JMS、JTA(Java Transaction API)などのさまざまなトランザクションテクノロジーとの統合を可能にします。PlatformTransactionManagerインターフェースがコア抽象化であり、Springがあらゆる基盤となるトランザクションテクノロジーと連携できるようにします。
コード例(Java/Spring Boot):
import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.beans.factory.annotation.Autowired; @Service public class AccountService { @Autowired private AccountRepository accountRepository; @Transactional // このメソッドをトランザクションとしてマーク public void transferFunds(Long fromAccountId, Long toAccountId, double amount) { Account fromAccount = accountRepository.findById(fromAccountId) .orElseThrow(() -> new RuntimeException(" Sender account not found")); Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> new RuntimeException(" Receiver account not found")); if (fromAccount.getBalance() < amount) { throw new RuntimeException("Insufficient funds"); } fromAccount.setBalance(fromAccount.getBalance() - amount); toAccount.setBalance(toAccount.getBalance() + amount); accountRepository.save(fromAccount); // 引き落とし後、入金前にエラーをシミュレート // if (true) throw new RuntimeException("Simulated error"); accountRepository.save(toAccount); } // トランザクション属性をカスタマイズできます @Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED) public Account getAccountDetails(Long accountId) { return accountRepository.findById(accountId) .orElse(null); } }
この例では、transferFundsメソッド内(シミュレートされたエラーを含む)でエラーが発生した場合、操作全体がロールバックされ、両方のaccountRepository.save呼び出しが取り消されることが保証されます。
アプリケーションシナリオ:
Springの@Transactionalは、さまざまなデータソース間で堅牢なデータ整合性を必要とするほとんどのアプリケーションに最適です。マイクロサービス、モノリシックアプリケーション、およびリレーショナルデータベース、メッセージキュー、その他のトランザクションリソースを使用するシステムで広く使用されています。
###SpriteCollision: ASP.NET Core トランザクション管理
ASP.NET Core、特にEntity Framework Core (EF Core) のようなツールは、トランザクションを管理するための柔軟な方法を提供します。あらゆるリソースにわたってSpringの単純さに直接対応する[Transactional]属性はありませんが、System.TransactionsとEF Coreは、強力な宣言的およびプログラム的なオプションを提供します。宣言的なトランザクションのような動作への一般的なアプローチは、EF Coreのユニットオブワーク機能またはTransactionScopeに依存することがよくあります。
原則と実装:
EF Coreを使用する場合、各DbContextインスタンスは暗黙的にユニットオブワークとして機能します。DbContextによって追跡された変更は、_dbContext.SaveChanges()が呼び出されたときにまとめてコミットされます。SaveChanges()の前にエラーが発生した場合、変更は永続化されません。複数操作、クロスサービスク、または分散トランザクションについては、System.Transactions.TransactionScopeが従来の.NETの方法です。
TransactionScopeは、アンビエントトランザクションコンテキストを作成します。そのスコープ内で開かれたIDbConnection(または他のトランザクションリソース)は、自動的にアンビエントトランザクションに参加します。scope.Complete()が呼び出されるとトランザクションがコミットされ、それ以外の場合はスコープが破棄されるときにロールバックされます。メソッドへの直接的な属性ではありませんが、そのusingブロック構造により、コンテキストによって「宣言的」になります。
コード例(C#/ASP.NET Core):
using System.Transactions; // TransactionScope用 using Microsoft.EntityFrameworkCore; using YourProject.Data; // DbContextがここにあると仮定 using YourProject.Models; public class AccountService { private readonly ApplicationDbContext _dbContext; public AccountService(ApplicationDbContext dbContext) { _dbContext = dbContext; } public void TransferFunds(long fromAccountId, long toAccountId, decimal amount) { // クロスオペレーション整合性のためのTransactionScopeの使用 using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { var fromAccount = _dbContext.Accounts.Find(fromAccountId); var toAccount = _dbContext.Accounts.Find(toAccountId); if (fromAccount == null || toAccount == null) { throw new InvalidOperationException("One or both accounts not found."); } if (fromAccount.Balance < amount) { throw new InvalidOperationException("Insufficient funds."); } fromAccount.Balance -= amount; toAccount.Balance += amount; _dbContext.SaveChanges(); // 両方のアカウントの変更はまとめてコミットされます。 // 最初.SaveChanges()の後、トランザクションスコープ内でエラーをシミュレート // if (true) throw new Exception("Simulated service error"); // 分散トランザクションや他のリソースが関与している場合、 // System.Transactionsをサポートしていれば自動的に参加します。 scope.Complete(); // トランザクションをコミット } // scope.Complete()が呼び出されない場合、トランザクションは暗黙的にロールバックされます } // EF CoreのSaveChangesはユニットオブワークです。 // このような単一データベース操作では、SaveChanges()で通常十分です。 public Account GetAccountDetails(long accountId) { return _dbContext.Accounts.Find(accountId); } }
TransactionScopeは宣言的な境界に近いものを提供しますが、メソッドのアノテーションというよりは、操作を「スコープ」することのほうが主です。EF Coreの場合、_dbContext.Database.BeginTransaction()と_dbContext.Database.CommitTransaction()は、より詳細な制御と明示的なプログラム管理を提供します。
アプリケーションシナリオ:
TransactionScopeは、同じプロセス内の複数のトランザクションリソース(例: 複数のデータベース、メッセージキュー)にわたる操作の原子性を確保するのに優れています。EF CoreのSaveChanges()は、単一データベース操作に適しています。EF Coreを多用するASP.NET Coreアプリケーションや、分散トランザクション機能が必要なアプリケーションは、これらのアプローチから恩恵を受けることができます。
EJB (Enterprise JavaBeans) トランザクション管理
Java EEプラットフォームの基盤となるコンポーネントモデルであるEJBは、アノテーションまたはデプロイメントディスクリプタを通じて、長年にわたり堅牢な宣言的なトランザクション管理を提供してきました。この分野では初期のソリューションの1つでした。
原則と実装:
EJBコンテナは、EJBコンポーネント(セッションBeanなど)のトランザクションを管理します。Springと同様に、EJBはプロキシ(またはインターセプタ)を使用してビジネスメソッドをラップします。クライアントがEJBコンポーネントのメソッドを呼び出すと、コンテナが呼び出しをインターセプトします。そのメソッド(またはクラス)に対して宣言されたトランザクション属性に基づいて、コンテナは新しいトランザクションを開始するか、既存のトランザクションに参加するか、またはトランザクションなしで実行します。
EJBは2種類のトランザクション管理をサポートしています。
- コンテナ管理トランザクション (CMT): EJBコンテナがトランザクションライフサイクルを管理します。これは
@TransactionAttributeのようなアノテーションを使用した宣言的なアプローチです。 - Bean管理トランザクション (BMT): EJB Bean自体がJTA API (
UserTransaction) を使用してプログラムでトランザクションライフサイクルを制御します。
宣言的なトランザクション管理のためにはCMTが使用されます。
コード例(Java EE/EJB):
import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Stateless // これをEJBセッションBeanとして示す public class AccountServiceEJB { @PersistenceContext // コンテナからEntityManagerを注入 private EntityManager entityManager; @TransactionAttribute(TransactionAttributeType.REQUIRED) // コンテナ管理トランザクション public void transferFunds(Long fromAccountId, Long toAccountId, double amount) { Account fromAccount = entityManager.find(Account.class, fromAccountId); Account toAccount = entityManager.find(Account.class, toAccountId); if (fromAccount == null || toAccount == null) { throw new RuntimeException("One or both accounts not found."); } if (fromAccount.getBalance() < amount) { throw new RuntimeException("Insufficient funds."); } fromAccount.setBalance(fromAccount.getBalance() - amount); toAccount.setBalance(toAccount.getBalance() + amount); // 変更はEntityManagerによって自動的に追跡され、コンテナによってコミットされます // if (true) throw new RuntimeException("Simulated EJB error"); } @TransactionAttribute(TransactionAttributeType.SUPPORTS) // 既存のトランザクションがあれば使用、なければトランザクションなし public Account getAccountDetails(Long accountId) { return entityManager.find(Account.class, accountId); } }
EJBの例では、@TransactionAttribute(TransactionAttributeType.REQUIRED)アノテーションは、コンテナにtransferFundsメソッドがトランザクション内で実行されることを保証するように指示します。トランザクションが既にアクティブな場合は参加し、そうでない場合はコンテナが新しいトランザクションを開始します。チェックされない例外が発生した場合、トランザクションはロールバック対象としてマークされます。
アプリケーションシナリオ:
EJBのCMTは、WildFly、GlassFish、WebLogic、WebSphereなどのJava EEアプリケーションサーバー上で構築されたエンタープライズグレードのアプリケーションに適しており、ビジネスロジックにEJBコンポーネントモデルを多用し、分散トランザクション管理(JTA)を含むアプリケーションサーバーが提供する豊富なサービスから恩恵を受けます。
比較と結論
3つのフレームワークすべてが、開発者が明示的にプログラムするのではなく、トランザクション動作を宣言できるようにすることで、トランザクション管理を簡素化することを目指しています。
- Spring (
@Transactional) は、AOPを活用してトランザクションプロキシを適用する、最も柔軟で広く採用されているアノテーション駆動型アプローチを提供します。そのPlatformTransactionManager抽象化により、さまざまなトランザクションテクノロジーや環境への適合性が非常に高く、モダンなJavaアプリケーションに最適な選択肢となっています。 - ASP.NET Core(
TransactionScopeとEF Coreを使用) は強力なメカニズムを提供しますが、一般的なトランザクション管理においては、SpringやEJBと比較して「宣言的」な属性ベースのアプローチがやや統一されていません。TransactionScopeは広範なスコープを包むのに優れており、EF Coreはデータベース操作に暗黙的なユニットオブワークセマンティクスを提供します。 - EJB (
@TransactionAttribute) は、この分野のベテランであり、包括的なエンタープライズプラットフォームの一部として、堅牢なコンテナ管理トランザクション(CMT)サポートを提供します。宣言的なトランザクション管理の先駆的なソリューションであり、従来のJava EEアプリケーションにとって強力な選択肢であり続けています。
構文や基盤となるメカニズム(AOPプロキシ対TransactionScope対EJBコンテナインターセプション)は異なりますが、最終的な目標は同じです。つまり、開発者の労力を最小限に抑えながら、データ整合性と原子性を確保することです。適切なアプローチの選択は、テクノロジースタック、アーキテクチャ要件、およびアプリケーションの特定のニーズに依存しますが、各フレームワークは、堅牢で宣言的なトランザクション管理の力を巧みに提供しています。

