JavaEE7をはじめよう(16) - CDI トランザクション
Java EE7から、CDI Beanに対する宣言的トランザクションが使用可能になった。
宣言的トランザクションの使用を宣言しておくと、メソッドの開始と同時に暗黙的にトランザクションを開始し、メソッドの終了時にトランザクションをコミットまたはロールバックできる。
宣言的トランザクションは、Java EE6まではコンテナ管理トランザクション(Container-Managed Transactions, CMT)として、EJBにしか設定できなかった。Java EE7からはCDI Beanに設定可能となったため、事実上あらゆるクラスに宣言的トランザクションを設定可能となった。
今回は、宣言的トランザクションの設定方法とサンプルを紹介する。
@Transactional
アノテーション
CDI Beanに対して宣言的トランザクションを設定するには、@javax.transaction.Transactional
アノテーションをクラスまたはメソッドに設定する。
クラスに設定した場合はクラス内の全メソッドがトランザクションの対象となる。
なお、@Transactinal
アノテーションはパッケージ名からわかるとおり、厳密にはCDIではなく、JTA(Java Transaction API)の一部であり、JTAの機能をCDIのインターセプター上で動作させるための仕組みである。
以下は、サンプルプログラムのビジネスロジックの一部である。2つのメソッドそれぞれがトランザクションの対象となる。 また、ロールバックの有無を確認するため引数の値に応じて、実行時例外およびチェック例外を発生させるようにしている。
// このクラスのメソッドを起点にトランザクションを開始する。 @Transactional @RequestScoped public class SampleService { // DAOをインジェクション @Inject private TeamDao dao; /** * 新しいチームを登録する * * 空文字の場合は実行時例外を、 * 引数が2文字以下の場合はチェック例外を発生させる。 * @param teamName チーム名 * @throws SampleException チーム名が2文字以下の場合 */ public void newTeam(String teamName) throws SampleException { Team t = new Team(); t.setName(teamName); dao.register(t); // 永続処理の実行後に、例外判定を行う。 if (teamName.equals("")) { throw new RuntimeException("uncheck exception throws."); } if (teamName.length() <= 2) { throw new SampleException("check exception throws."); } } public List<Team> allTeams() { return dao.findAll(); } }
コミット・ロールバックのルール
@Transactinal
アノテーションで設定した宣言的トランザクションは、デフォルトでは以下のルールで動作する。
上記ルールに関わりなく、特定の例外に対してロールバックを強制させるには、同アノテーションのrollbackOn
属性でその例外のクラスを指定する。
逆に、特定の例外をロールバック対象外にするには、dontRollbackOn
属性を使用する。
とはいえ、これらの属性を設定すると煩雑になるので、なるべくならデフォルトのルールのみで問題なくアプリケーションが動くように設計を行うべきだろう。
トランザクション境界
前述のSampleService
は、TeamDao
のクラスをインジェクションしている。
以下がTeamDao
のソースである。
// デフォルト(Required)のため、同一トランザクションで実行される。 @Transactional @Dependent public class TeamDao { @Inject private EntityManager em; /** 登録 */ public void register(Team team) { em.persist(team); em.flush(); } public List<Team> findAll() { return em.createQuery( "select t from Team t order by t.name", Team.class) .getResultList(); } }
ちなみに、ここで@Inject
を宣言しているEntityManager
は、以前の記事で紹介したCDIのProducerフィールドにより取得した、コンテナ管理のEntityManager
である。
コンテナ管理のEntityManager
も宣言的トランザクション内で動作する。
さて、ここではTeamDao
にも@Transactional
アノテーションを付与している。
この場合、トランザクションはどのように動作するだろうか。
その答えは、@Transaction
アノテーションのvalue
属性に依存する。value
属性はトランザクションの属性を示すもので、Transactional.TxType
型のenum
である。
この属性はトランザクションにどのような境界を設定するかを表し、自身のメソッドと外側、すなわち自身のメソッドを呼び出したメソッドのトランザクションの有無に従って以下のような動作をする。
TxTypeの値 | 内容 |
---|---|
REQUIRED |
デフォルト。外側のメソッドがトランザクション実行中なら、そのトランザクションを使用する。そうでない場合、新しくトランザクションを作成する。 |
REQUIRES_NEW |
外側のメソッドのトランザクションの有無に関係なく、トランザクションを作成する。意図的にトランザクションを分割する場合に使用する。 |
SUPPORTS |
外側のメソッドのトランザクションがあればそれを使用する。無い場合、トランザクションなしで実行する(SELECT 以外の操作を行うと、失敗することになる。) |
MANDATORY |
外側のメソッドにトランザクションがあればそれを使用する。無い場合、例外を発生させる。 |
NOT_SUPPORTED |
トランザクションを使用しない設定。外側のメソッドがトランザクションを使用しているかに関わらず、トランザクションなしで実行される。 |
NEVER |
トランザクションを使用しない設定。外側のメソッドがトランザクションを使用している場合で呼び出すと例外を発生させる。 |
通常使用するのは最初の2つくらいであろう。
今回のケースではどちらのクラスも、@Transactional
のみを記述しているので、TxType.REQUIRED
を指定したことになる。
よって、SampleService
とTeamDao
の一連の処理は単一のトランザクションで動作する。
(TeamDao
の宣言を@Transactional(TxType.REQUIRES_NEW)
と指定した場合には、2つのトランザクションで動作する。)
サンプル
上記のソースの実行はこちらで行う事ができる。
入力値をテーブルに挿入する処理で、入力値に応じて、実行時例外・チェック例外を発生させる。実行時例外の場合はロールバックを行う(テーブルの内容が変わらない)ことを確認できるはずだ。
ソースはこちらにある。
注意点
実行時例外によってロールバックが行われた際、呼び出し元にはjavax.transaction.TransactionalException
という実行時例外が投げられる。
上記のサンプルで実行時例外を起こすとわかると思うが、現状のglassfish 4.1 ではロールバックを起こす処理に不具合があり、TransactionalException
の原因となった例外(ここでは、SampleService
で発生させたRuntimeException
) を伝播させていない。このため、根本原因を解析したり、ログを残す場合に問題となる可能性がある。
不具合の内容はGLASSFISH-21172 で公開されており、
コメント上では次回のglassfish のアップデートで修正するとしている。
また、この問題に対応された方もいらっしゃるので、紹介しておく。
GlassFish4.1をなおしてみた - 見習いプログラミング日記
他にも対策可能な方法としては、ログを出力するようなインターセプターを用意し、@Transactinal
インターセプターの後に動かす方法が考えられる。
インターセプターについては、今後解説する予定である。
EJB との使い分け
@Transactional
アノテーションでできることは、EJBのコンテナ管理トランザクション(CMT)とほぼ同じである。
宣言的トランザクションはEJBの主要な機能の1つのため、簡単なアプリケーションであれば EJB を使う必要がない(実際、サンプルプログラムでもほとんどEJBは使用していない)。
現在のところ、EJBでなければできないこと(あるいは、実現可能だが工夫が必要なもの)は以下のようなものがある。
- リモート呼び出し (別のVMなどからのリモートからのメソッド実行)
- セキュリティ設定 (サーバーに設定したロールなどに基づく認可設定)
- JMS (メッセージング処理)
- 非同期処理
- タイマー処理 (EJBでは、特定の時刻や間隔でメソッドを実行することができる)
- 対話の保持 (いわゆるステートフルセッションビーン。 CDIのコンテキストも似たような機能である。)
よって、普通はCDI Beanを使用し、上記の機能を使用する場合のみEJBを使用するか、あるいはドメイン・ビジネスロジックなどの特定の役割をこなすクラスを一律EJBとするかは、設計によって決める必要があるだろう。
いずれにせよ、重要なのは CDI と EJB は排他的な機能ではなく共存可能であることだ。 EJBを使用する場合でも、EJB + CDI という構成にすることで、柔軟な構成となる。
CDIはあらゆる機能を@Inject
で使用可能にしており、 その中には EJBも含まれる。@Inject
を使用してインジェクションをする側は、インジェクションされるものが、CDI Bean でも EJBでも気にする必要がないというのがCDIの利点である。
(CDI以前は、@EJB
, @Resouce
, @PersistenceContext
のような対象に合わせたDI用のアノテーションを宣言する必要があったが、 CDIによって@Inject
に一本化されるようになった。)
まとめ
今回はCDIによる宣言的トランザクションの使用方法について紹介した。
この機能を使うことで、サーブレット、CDI Bean, JPA を組み合わせて簡単なWebアプリケーションを構築できる。
EJB がなくとも 宣言的トランザクションを使用したアプリケーションを作成できる点で、設計の自由度が増したといえるだろう。
[前多 賢太郎]