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

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

8.1. はじめに

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

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

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

  • ステートフルセッション bean の寿命はウェブ層でコード経由により手作業で管理されなければなりません (これは微妙な問題であり実際にはかなり困難であることがわかります)。

  • 同じ楽観的トランザクション内のステートフルコンポーネント間での永続コンテキストの伝播は可能ですが簡単ではありません。

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

8.2. Seam 管理トランザクション

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

ただし、 この方法には問題が 1 つあります。 Seam アプリケーションは単一のメソッドコールからセッション bean へのリクエストに対して全データアクセスを行わない可能性があります。

  • このリクエストにはいくつかの疎結合コンポーネントによる処理を必要とする場合があります。 それぞれのコンポーネントが web 層から個別に呼び出されます。 Seam ではリクエストごと web 層から EJB コンポーネントへのコールが複数あるのはよく見られることです。

  • ビューのレンダリングには関連の遅延フェッチが必要な場合があります。

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

Hibernate ユーザーはこの問題を回避するため

このパターンは通常、 リクエスト全体にスパンする単一トランザクションとして実装されます。 この実装ではいくつかの問題があります。 もっとも深刻となる問題は、 トランザクションをコミットするまでそれが成功なのかどうか全く確認できないことです — ただし、 「ビュー内のオープンセッション」トランザクションがコミットされるようになると、 ビューは完全にレンダリングされるので、 レンダリングされるレスポンスはすでにクライアントにフラッシュされている場合があります。 ユーザーのトランザクションが成功しなかったことをユーザーに知らせるにはどうしたらよいでしょうか。

Seam はトランザクション個別の問題と関連フェッチの問題の両方を解決しながら、 「ビュー内のオープンセッション」で問題を回避します。 解決法は 2 つに分けられます。

  • トランザクションに対してスコープされるのではなく、 対話に対してスコープされる拡張永続コンテキストを使用する

  • 1 リクエストに対して 2 つのトランザクションを使用する、 1 番目はモデル値の更新フェーズの開始からアプリケーション呼び出しフェーズの終わりまでスパンし、 2 番目はレスポンスのレンダリングフェーズをスパンする

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

8.2.1. Seam 管理トランザクションを有効にする

Seam managed transactions を利用するには、 SeamPhaseListener の代わりに TransactionalSeamPhaseListener を使用する必要があります。

<lifecycle>
     <phase-listener>
        org.jboss.seam.jsf.TransactionalSeamPhaseListener
    </phase-listener>
</lifecycle>

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

8.3. Seam 管理の永続コンテキスト

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

いずれの場合では、 コンポーネントで managed persistence context (JPA 用) または managed session (Hibernate 用) のいずれかを使用する必要があります。 Seam 管理永続コンテキストは単純にビルトインの Seam コンポーネントで対話コンテキストで EntityManager または Session のインスタンスを管理します。 @In でインジェクトすることができます。

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

8.3.1. JPA で Seam 管理の永続コンテキストを使用する

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

<core: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;

8.3.2. Seam 管理の Hibernate セッションを使用する

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

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

<core: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;

8.3.3. Seam 管理の永続コンテキストと微小な対話

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; //a Seam-managed persistence context

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

これで claim オブジェクトは残りの対話の永続コンテキストによる管理を維持します。 このクレームに変更を加えることができます。

public void addPartyToClaim() {
    Party party = ....;
    claim.addParty(party);
}

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

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

8.4. JPA 「デリゲート」を使用する

EntityManager インターフェースにより getDelegate() メソッドを介してベンダー固有の API にアクセスすることができます。 必然的に、 Hibernate が最も関心の高いベンダーとなり、 org.hibernate.Session が最も強力となるデリゲートインターフェースになります。 これ以外を使用するのがばかばかしくなるほどです。 偏見無しによいので試してみてください。

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

@In EntityManager entityManager;

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

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

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

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

@In Session session;

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

8.5. EJB-QL/HQL で EL を使用する方法

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 インジェクション攻撃に対して脆弱となります。)

8.6. Hibernate フィルタを使用する

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

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

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

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

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