SeamFramework.orgCommunity Documentation

第9章 Seam とオブジェクト/リレーショナルマッピング

9.1. はじめに
9.2. Seam 管理トランザクション
9.2.1. Seam 管理トランザクションを無効にする
9.2.2. Seamトランザクションマネージャを設定する
9.2.3. トランザクションの同期化
9.3. Seam 管理の永続コンテキスト
9.3.1. JPA で Seam 管理の永続コンテキストを使用する
9.3.2. Seam 管理の Hibernate セッションを使用する
9.3.3. Seam 管理の永続コンテキストとアトミックな対話
9.4. JPA 「デリゲート」を使用する
9.5. EJB-QL/HQL で EL を使用する方法
9.6. Hibernate フィルタを使用する

Seam は EJB 3.0 で導入される Java Persistence API および Hibernate3 の二つの最も一般的な Java 用永続アーキテクチャに対して広範なサポートを提供します。 Seam 固有の状態管理アーキテクチャにより、 いかなるウェブアプリケーションフレームワークからも高度な ORM 統合を実現します。

Seam は、 旧世代の Java アプリケーションアーキテクチャの典型であるステートレス性に悩む Hibernate チームのフラストレーションから生まれました。 Seam の状態管理アーキテクチャは元々、 永続性に関する問題の解決を目的として設計されました — 特に 楽観的なトランザクションの処理 に関連する問題。 スケーラブルなオンラインアプリケーションは常に楽観的なトランザクションを使用します。 アトミックな (データベース/JTA) レベルのトランザクションは、 アプリケーションがごく少数の並列クライアントのみをサポートするよう設計されていない限りユーザーのインタラクションをまたがるべきではありません。 しかし、 目的とするほぼすべての作業は、まずユーザーにデータを表示し次に少し遅れて同じデータを更新するというものです。 このため、 Hibernate は楽観的なトランザクションにまたがる永続コンテキストというアイデアに対応するよう設計されました。

残念ながら、 Seam や EJB 3.0 より以前の「ステートレス」と呼ばれるアーキテクチャには楽観的なトランザクションを表現するための構成概念がありませんでした。 このため、 代わりにアトミックなトランザクションに対してスコープされる永続コンテキストを提供していました。 当然、 これはユーザーにとって多くの問題を引き起こし、 また Hibernate に関するユーザーからの最大の苦情、恐怖の LazyInitializationException の原因でもあります。 ここで必要なのはアプリケーション層で楽観的トランザクションを表現する構成概念なのです。

EJB 3.0 はこの問題を認識し、 コンポーネントの寿命に対してスコープされる 拡張永続コンテキスト を持ったステートフルなコンポーネント (ステートフルセッション Bean) というアイデアを導入します。 これは問題に関して完全なソリューションではありませんが (それ自体は便利な構成です) 、 二つの問題があります。

Seam は、 対話および対話に対してスコープされるステートフルセッション Bean コンポーネントを提供することにより 1 番目の問題を解決します。 (ほとんどの対話は実際にはデータ層で楽観的トランザクションを表示します。) 永続コンテキストの伝播を必要としないような多くのシンプルなアプリケーション (Seam ブッキングデモなど) にはこれで十分です。 各対話内で疎に作用しあっているコンポーネントを多く持っているようなもう少し複雑なアプリケーションの場合、 コンポーネント群全体への永続コンテキストの伝播は重要な問題となります。 このため、 Seam は EJB 3.0 の永続コンテキスト管理モデルを拡張して対話スコープの拡張永続コンテキストを提供しています。

EJB セッション Bean は宣言型トランザクション管理を特長としています。 EJB コンテナは Bean が呼び出されると透過的にトランザクションを起動し、 呼出しが終了するとトランザクションも終了させることが可能です。 JSF アクションリスナーとして動作するセッション Bean メソッドを記述する場合、 そのアクションに関連するすべての作業を 1 つのトランザクションで行うことができ、 アクションの処理が完了したら必ずコミットまたはロールバックされるようにすることができます。 これは素晴らしい機能であり、 いくつかの Seam アプリケーションに必要とされるものはこれだけです。

ただし、 この方法には問題が 1 つあります。 Seam アプリケーションは一つの要求において、一つのセッション Bean への単一のメソッド呼び出しですべてのデータアクセスを終えるわけではない可能性があります。

1 要求ごとのトランザクション数が多くなると、 使用しているアプリケーションが多くの並列要求を処理している際にそれだけ多くのアトミック性と独立性の問題に遭遇する可能性が高くなります。 書き込み動作はすべて、 必ず、 同じトランザクション内で起こらなければならないからです。

Hibernate ユーザーはこの問題を回避するため 「open session in view」 パターンを開発しました。 Spring のようなフレームワークはトランザクションスコープの永続コンテキストを使用しており、 ビューのレンダリングはフェッチされない関連がアクセスされると LazyInitializationException を引き起こすため、 Hibernate コミュニティでは「open session in view」は歴史的により重要でした。

このパターンは通常、 要求全体にまたがる単一トランザクションとして実装されます。 この実装ではいくつかの問題があります。 もっとも深刻となる問題は、 トランザクションをコミットするまでそれが成否を全く確認できないことです — 「open session in view」トランザクションがコミットされる前にもかかわらず、しかし、 ビューは完全にレンダリングされているので、 レンダリングされた応答はすでにクライアントに送出されている可能性があります。 ユーザーのトランザクションが成功しなかったことをユーザーに知らせるには一体どうしたらよいのでしょうか。

Seam はトランザクション独立性の問題と関連フェッチの問題の両方を解決しながら、 「open session in view」に関する問題を回避します。 解決法は二つに分けられます。

次のセクションでは、 対話スコープの永続コンテキストの設定方法について説明していきますが、 まず最初に Seam トランザクション管理を有効にする方法を説明しておく必要があります。 Seam トランザクション管理なしで対話スコープの永続コンテキストを使用することができ、 また Seam 管理永続コンテキストを使用していない場合でも Seam トランザクション管理を利用すると便利なことがあるので留意しておいてください。 ただし、 この二つの機能は連携して動作するよう設計されているため、 併用する方が最適です。

Seam トランザクション管理は EJB 3.0 コンテナ管理の永続コンテキストを使用している場合でも役に立ちますが、 Java EE 5 環境の外側で Seam を使用する場合、 あるいはこれ以外の Seam 管理の永続コンテキストを使用するような環境で特に役立ちます。

Seam はトランザクションでの開始、 コミット、 ロールバック、 同期などの動作にトランザクション管理の抽象化を提供します。 デフォルトでは Seam はコンテナ管理やプログラムでの EJB トランザクションを統合する JTA トランザクションコンポーネントを使用します。 Java EE 5 の環境で作業している場合は components.xml に EJB 同期化コンポーネントをインストールしてください。


<transaction:ejb-transaction />

ただし、 EE 5 コンテナ以外で作業している場合は Seam が使用するトランザクション同期化メカニズムを自動検出しようとします。 Seam が正しいトランザクションの同期化を検出できない場合は次のいずれかを設定する必要があるかもしれません。

components.xml に次を追加して JPA RESOURCE_LOCAL トランザクション管理を設定します。 #{em}persistence:managed-persistence-context コンポーネント名です。 管理永続コンテキスト名が entityManager なら entity-manager 属性を省略することができます。 (Seam 管理の永続コンテキスト を参照)


<transaction:entity-transaction entity-manager="#{em}"/>

Hibernate 管理トランザクションを設定するには 次を components.xml で宣言します。 #{hibernateSession} はプロジェクトの persistence:managed-hibernate-session コンポーネント名です。 管理 Hibernate セッション名が session なら session 属性を省略することができます。 (Seam 管理の永続コンテキスト を参照)


<transaction:hibernate-transaction session="#{hibernateSession}"/>

Seam 管理トランザクションを明示的に無効にするには次を components.xml で宣言します。


<transaction:no-transaction />

Spring 管理トランザクションについては Spring の PlatformTransactionManagement を使用する を参照してください。

Seam を Java EE 5 環境の外で使用している場合、 コンテナによる永続コンテキストのライフサイクルの管理は期待できません。 EE 5 環境であっても、 単一の対話の範囲内で連携する多くの疎結合コンポーネントを持つ複雑なアプリケーションがあるかもしれず、 この場合にはコンポーネント間での永続コンテキストの伝播が簡単ではなくエラーが発生しやすい場合があります。

いずれの場合でも、 コンポーネントで 管理永続コンテキスト (JPA 用) または 管理セッション (Hibernate 用) のいずれかを使用する必要があります。 Seam 管理永続コンテキストは単に組み込みの Seam コンポーネントです。 対話コンテキストで EntityManager または Session のインスタンスを管理します。 @In でインジェクトすることができます。

Seam 管理の永続コンテキストはクラスタ化された環境で非常に効率的です。 EJB 3.0 の仕様ではコンテナがコンテナ管理拡張永続コンテキストに対して行うことが許可されていないような最適化を Seam は実行することができます。 ノード間の永続コンテキストの状態を複製することなく拡張永続コンテキストの透過的なフェールオーバーをサポートします。 (この見過ごされてしまった点については、 次回の EJB 仕様のリビジョンで修正したいと考えています。)

管理永続コンテキストの設定は簡単です。 components.xml 内に次のように記述します。


<persistence:managed-persistence-context name="bookingDatabase" 
                                  auto-create="true"
                   persistence-unit-jndi-name="java:/EntityManagerFactories/bookingData"/>

この設定により対話スコープの bookingDatabase という名前の Seam コンポーネントが作成され、 JNDI 名 java:/EntityManagerFactories/bookingData を持つ永続ユニット (EntityManagerFactory インスタンス) の EntityManager インスタンスの寿命を管理します。

当然、 EntityManagerFactory が JNDI にバウンドされたことを確認する必要があります。 JBoss では、 次のプロパティ設定を persistence.xml に追加すると確認を行うことができます。


<property name="jboss.entity.manager.factory.jndi.name" 
          value="java:/EntityManagerFactories/bookingData"/>

これで次のように EntityManager をインジェクトできます。

@In EntityManager bookingDatabase;

EJB 3 を使用していてクラスまたはメソッドに @TransactionAttribute(REQUIRES_NEW) をマークするとトランザクションと永続コンテキストはこのオブジェクトでのメソッドコールには伝播されないはずです。 ただし、 Seam 管理の永続コンテキストは対話内でいずれのコンポーネントにも伝播されるため REQUIRES_NEW とマークされたメソッドにも伝播されます。 したがって、 メソッドに REQUIRES_NEW をマークする場合は @PersistenceContext を使ってエンティティマネージャにアクセスしてください。

Seam 管理 Hibernate セッションも同様にcomponents.xml で次のように記述することができます。


<persistence:hibernate-session-factory name="hibernateSessionFactory"/>

<persistence:managed-hibernate-session name="bookingDatabase" 
                                auto-create="true"
                  session-factory-jndi-name="java:/bookingSessionFactory"/>

java:/bookingSessionFactoryhibernate.cfg.xml で指定されるセッションファクトリ名にします。


<session-factory name="java:/bookingSessionFactory">
    <property name="transaction.flush_before_completion"
>true</property>
    <property name="connection.release_mode"
>after_statement</property>
    <property name="transaction.manager_lookup_class"
>org.hibernate.transaction.JBossTransactionManagerLookup</property>
    <property name="transaction.factory_class"
>org.hibernate.transaction.JTATransactionFactory</property>
    <property name="connection.datasource"
>java:/bookingDatasource</property>
    ...
</session-factory
>

Seam はセッションをフラッシュしないので、 hibernate.transaction.flush_before_completion を常に有効にしてセッションが JTA トランザクションのコミットより先に自動的にフラッシュされるようにしなければならないので注意してください。

これで、 次のコードを使って JavaBean コンポーネントに管理 Hibernate Session をインジェクトできます。

@In Session bookingDatabase;

merge() 演算を使用したり、 各要求の冒頭でデータを再ロードしたり、 LazyInitializationExceptionNonUniqueObjectException と格闘しなくとも、 対話にスコープされる永続コンテキストによりサーバーに対して複数の要求にまたがる楽観的なトランザクションをプログラムすることができるようになります。

楽観的トランザクション管理では楽観的ロックでトランザクションの隔離と一貫性を実現します。幸い、 Hibernate と EJB 3.0 いずれも @Version アノテーションを提供することで楽観的ロックの使用を容易にしています。

デフォルトでは、 永続コンテキストは各トランザクションの終わりでフラッシュされます (データベースと同期される)。 これが目的の動作である場合もありますが、 すべての変更はメモリに保持され対話が正常に終了したときにのみデータベースに書き込まれる動作を期待することの方が多いでしょう。 これにより真にアトミックな対話を可能にします。 EJB 3.0 エキスパートグループの中の JBoss、 Sun、 Sybase 以外の特定のメンバーによって長期的な見通しを考慮に入れず短絡的な決定がなされてしまったため、 EJB 3.0 永続を使用したアトミックな対話の実装を行うシンプルで使用に適したポータブルな方法が現在ありません。 ただし、 Hibernate では仕様により定義される FlushModeType に対するベンダー拡張としてこの機能を提供しています。 また、 他のベンダーもじきに同様の拡張を提供するだろうことを期待しています。

Seam では対話の開始時に FlushModeType.MANUAL を指定することができます。 現在は Hibernate が永続を実現する構成要素である場合にのみ機能しますが、 他のベンダーによる同等の拡張もサポートする予定です。

@In EntityManager em; //Seam-managed persistence context


@Begin(flushMode=MANUAL)
public void beginClaimWizard() {
    claim = em.find(Claim.class, claimId);
}

これで claim オブジェクトは対話の残りの間、 永続コンテキストによって管理され続けます。 この claim に変更を加えることができます。

public void addPartyToClaim() {

    Party party = ....;
    claim.addParty(party);
}

ただし、 これらの変更は明示的にフラッシュが発生するよう強制するまではデータベースに対してフラッシュされません。

@End

public void commitClaim() {
    em.flush();
}

当然、 pages.xml から flushModeMANUAL にセットすることができます。 たとえばナビゲーション規則では以下のようになります。


<begin-conversation flush-mode="MANUAL" />

いずれの Seam 管理永続コンテキストに対しても手動によるフラッシュモードを使用するよう設定することができます。

<components xmlns="http://jboss.com/products/seam/components"
   xmlns:core="http://jboss.com/products/seam/core">
   <core:manager conversation-timeout="120000" default-flush-mode="manual" />
</components
>

EntityManager インタフェースにより getDelegate() メソッドを通じてベンダー固有の API にアクセスすることができます。 必然的に、 Hibernate が最も関心の高いベンダーとなり、 org.hibernate.Session が最も強力となるデリゲートインタフェースになります。 これ以外を使用するのがばかばかしくなるほどです。 これは偏見抜きの意見です。 別の JPA プロバイダを使用しなければならない場合は 代替の JPA プロバイダを使用する をご覧ください。

ただし、 Hibernate またはそれ以外のものいずれを使用するかに限らず、 いずれは Seam コンポーネントでデリゲートを使用したくなる場合がくるでしょう。 以下にその一例を示します。

@In EntityManager entityManager;


@Create
public void init() {
    ( (Session) entityManager.getDelegate() ).enableFilter("currentVersions");
}

型キャストは Java 言語の中でも間違いなく繁雑な構文になるため、 できる限り避けるのが一般的です。 デリゲートで取得する別の方法を次に示します。 まず、 以下の行を components.xml に追加します。


<factory name="session" 
         scope="STATELESS" 
         auto-create="true" 
         value="#{entityManager.delegate}"/>

これでセッションを直接インジェクトできるようになります。

@In Session session;


@Create
public void init() {
    session.enableFilter("currentVersions");
}

Seam 管理の永続コンテキストを使用する場合や @PersistenceContext を使ってコンテナ管理の永続コンテキストをインジェクトする場合、 Seam は EntityManager または Session オブジェクトをプロキシします。 これにより、 EL 式をクエリー文字列内で安全かつ効果的に使用することができるようになります。 たとえば、 次を見てください。

User user = em.createQuery("from User where username=#{user.username}")

         .getSingleResult();

上記の例は、 以下の例と同等になります。

User user = em.createQuery("from User where username=:username")

         .setParameter("username", user.getUsername())
         .getSingleResult();

当然、 次のようには絶対に記述しないでください。

User user = em.createQuery("from User where username=" + user.getUsername()) //BAD!

         .getSingleResult();

(効率が悪く、 SQL インジェクション攻撃に対して脆弱となります。)

Hibernate 固有の斬新な機能が フィルタ になります。 フィルタによりデータベース内のデータ表示に制限を与えることができるようになります。 フィルタについては Hibernate のドキュメントで詳細に説明されています。 ここでは、 フィルタを Seam アプリケーションに統合する簡単な方法を記載しておくのがよいだろうと思います。 特に Seam Application Framework でうまく動作する方法を説明します。

Seam 管理の永続コンテキストはフィルタの一覧を定義することができます。 これらは EntityManager や Hibernate Session がはじめて作成されたときに有効になります。 (当然、 Hibernate が永続を実現する構成要素である場合にのみ使用できます。)


<persistence:filter name="regionFilter">
    <persistence:name
>region</persistence:name>
    <persistence:parameters>
        <key
>regionCode</key>
        <value
>#{region.code}</value>
    </persistence:parameters>
</persistence:filter>

<persistence:filter name="currentFilter">
    <persistence:name
>current</persistence:name>
    <persistence:parameters>
        <key
>date</key>
        <value
>#{currentDate}</value>
    </persistence:parameters>
</persistence:filter>

<persistence:managed-persistence-context name="personDatabase"
    persistence-unit-jndi-name="java:/EntityManagerFactories/personDatabase">
    <persistence:filters>
        <value
>#{regionFilter}</value>
        <value
>#{currentFilter}</value>
    </persistence:filters>
</persistence:managed-persistence-context
>