エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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ではなく、JTAJava 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を指定したことになる。 よって、SampleServiceTeamDaoの一連の処理は単一のトランザクションで動作する。
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とするかは、設計によって決める必要があるだろう。

いずれにせよ、重要なのは CDIEJB は排他的な機能ではなく共存可能であることだ。 EJBを使用する場合でも、EJB + CDI という構成にすることで、柔軟な構成となる。

CDIはあらゆる機能を@Injectで使用可能にしており、 その中には EJBも含まれる。@Injectを使用してインジェクションをする側は、インジェクションされるものが、CDI Bean でも EJBでも気にする必要がないというのがCDIの利点である。
CDI以前は、@EJB, @Resouce, @PersistenceContext のような対象に合わせたDI用のアノテーションを宣言する必要があったが、 CDIによって@Injectに一本化されるようになった。)

まとめ

今回はCDIによる宣言的トランザクションの使用方法について紹介した。
この機能を使うことで、サーブレットCDI Bean, JPA を組み合わせて簡単なWebアプリケーションを構築できる。 EJB がなくとも 宣言的トランザクションを使用したアプリケーションを作成できる点で、設計の自由度が増したといえるだろう。

[前多 賢太郎]