Hibernate.orgCommunity Documentation

第14章 インターセプタとイベント

14.1. インターセプタ
14.2. イベントシステム
14.3. Hibernate の宣言的なセキュリティ

アプリケーションが Hibernate の内部で発生するイベントに対応できると役に立つことがあります。ある種の一般的な機能を実装できるようになり、また Hibernate の機能を拡張することもできるようになります。

Interceptor インターフェースを使って、セッションからアプリケーションへコールバックをすることができます。これにより永続オブジェクトの保存、更新、削除、読み込みの前に、アプリケーションがプロパティを検査したり操作したりできるようになります。これは監査情報の追跡に利用できます。下の例で InterceptorAuditable が作成されると自動的に createTimestamp を設定し、 Auditable が更新されると自動的に lastUpdateTimestamp プロパティを更新します。

Interceptor を直接実装したり、 (さらによいのは) EmptyInterceptor を拡張したりできます。

package org.hibernate.test;


import java.io.Serializable;
import java.util.Date;
import java.util.Iterator;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Transaction;
import org.hibernate.type.Type;
public class AuditInterceptor extends EmptyInterceptor {
    private int updates;
    private int creates;
    private int loads;
    public void onDelete(Object entity,
                         Serializable id,
                         Object[] state,
                         String[] propertyNames,
                         Type[] types) {
        // do nothing
    }
    public boolean onFlushDirty(Object entity,
                                Serializable id,
                                Object[] currentState,
                                Object[] previousState,
                                String[] propertyNames,
                                Type[] types) {
        if ( entity instanceof Auditable ) {
            updates++;
            for ( int i=0; i < propertyNames.length; i++ ) {
                if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
                    currentState[i] = new Date();
                    return true;
                }
            }
        }
        return false;
    }
    public boolean onLoad(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types) {
        if ( entity instanceof Auditable ) {
            loads++;
        }
        return false;
    }
    public boolean onSave(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types) {
        if ( entity instanceof Auditable ) {
            creates++;
            for ( int i=0; i<propertyNames.length; i++ ) {
                if ( "createTimestamp".equals( propertyNames[i] ) ) {
                    state[i] = new Date();
                    return true;
                }
            }
        }
        return false;
    }
    public void afterTransactionCompletion(Transaction tx) {
        if ( tx.wasCommitted() ) {
            System.out.println("Creations: " + creates + ", Updates: " + updates, "Loads: " + loads);
        }
        updates=0;
        creates=0;
        loads=0;
    }
}

インターセプタには二種類あります: Session スコープのものと SessionFactory スコープのものです。

Session スコープのインターセプタは、セッションをオープンするときに指定します。 Interceptor を引数に取る SessionFactory.openSession() のオーバーロードメソッドの一つを使います。

Session session = sf.openSession( new AuditInterceptor() );

SessionFactory スコープのインターセプタは Configuration オブジェクトを使って登録します。これは SessionFactory の構築よりも優先されます。この場合、提供されるインターセプタは SessionFactory からオープンされたすべてのセッションに適用されます。これは使用するインターセプタを明示的に指定してセッションをオープンしない限り、そうなります。 SessionFactory スコープのインターセプタはスレッドセーフでなければなりません。複数のセッションが (潜在的に) このインターセプタを同時並行で使用することになるため、セッション固有の状態を格納しないように気をつけてください。

new Configuration().setInterceptor( new AuditInterceptor() );

永続化層で特定のイベントに対応しなければならない場合、 Hibernate3 の イベント アーキテクチャを使うこともできます。イベントシステムはインターセプタと一緒に使うか、またはインターセプタの代わりとして使うことができます。

本質的に Session インターフェースのすべてのメソッドは、1個のイベントと相互に関連します。例えば LoadEventFlushEvent などがあります (定義済みのイベント型の一覧については、 XML 設定ファイルの DTD や org.hibernate.event パッケージを調べてください) 。リクエストがこれらのメソッドの1つから作られるとき、 Hibernate の Session は適切なイベントを生成し、そのイベント型に設定されたイベントリスナに渡します。すばらしいことに、これらのリスナはそのメソッドと同じ処理を実装します。とはいえ、リスナインターフェースの一つを自由にカスタム実装できます (つまり、 LoadEvent は登録された LoadEventListener インターフェースの実装により処理されます)。その場合、その実装には Session から作られたどのような load() リクエストをも処理する責任があります。

リスナは事実上シングルトンであると見なせます。つまりリスナはリクエスト間で共有されるため、インスタンス変数として状態を保持するべきではないということです。

カスタムリスナは処理したいイベントについて適切なインターフェースを実装するべきです。便利な基底クラスのうちの一つを継承してもよいです (または Hibernate がデフォルトで使用するイベントリスナを継承してもよいです。すばらしいことに、この目的のために非 final として宣言されています) 。カスタムリスナは Configuration オブジェクトを使ってプログラムから登録するか、 Hibernate の XML 設定ファイルで指定できます (プロパティファイルで宣言的に設定する方法はサポートされていません) 。カスタムロードイベントリスナの例を示します:

public class MyLoadListener implements LoadEventListener {

    // this is the single method defined by the LoadEventListener interface
    public void onLoad(LoadEvent event, LoadEventListener.LoadType loadType)
            throws HibernateException {
        if ( !MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() ) ) {
            throw MySecurityException("Unauthorized access");
        }
    }
}

デフォルトリスナ以外のリスナを使うには、 Hibernate への設定も必要です:


<hibernate-configuration>
    <session-factory>
        ...
        <event type="load">
            <listener class="com.eg.MyLoadListener"/>
            <listener class="org.hibernate.event.def.DefaultLoadEventListener"/>
        </event>
    </session-factory>
</hibernate-configuration
>

またその他に、プログラムで登録する方法もあります:

Configuration cfg = new Configuration();

LoadEventListener[] stack = { new MyLoadListener(), new DefaultLoadEventListener() };
cfg.EventListeners().setLoadEventListeners(stack);

リスナを宣言的に登録すると、そのリスナのインスタンスを共有できません。複数の <listener/> 要素で同じクラス名が使われると、それぞれの参照はそのクラスの別々のインスタンスを指すことになります。リスナ型の間でリスナインスタンスを共有する必要があれば、プログラムで登録する方法を採らなければなりません。

なぜインターフェースを実装して、特化した型を設定時に指定するのでしょうか?リスナの実装クラスに、複数のイベントリスナインターフェースを実装できるからです。登録時に追加で型を指定することで、カスタムリスナの on/off を設定時に簡単に切り替えられます。

一般的に Hibernate アプリケーションの宣言的なセキュリティは、セッションファサード層で管理します。現在、 Hiberenate3 は JACC で許可し、さらに JAAS で認証したアクションを許しています。これはイベントアーキテクチャの最上位に組み込まれているオプションの機能です。

まず最初に、適切なイベントリスナを設定して JAAS 認証を使えるようにしなければなりません。


<listener type="pre-delete" class="org.hibernate.secure.JACCPreDeleteEventListener"/>
<listener type="pre-update" class="org.hibernate.secure.JACCPreUpdateEventListener"/>
<listener type="pre-insert" class="org.hibernate.secure.JACCPreInsertEventListener"/>
<listener type="pre-load" class="org.hibernate.secure.JACCPreLoadEventListener"/>

特定のイベント型に対してちょうど一つのリスナがあるとき、 <listener type="..." class="..."/><event type="..."><listener class="..."/></event> の簡略形に過ぎないことに注意してください。

次に、同じく hibernate.cfg.xml でロールにパーミッションを与えてください:


<grant role="admin" entity-name="User" actions="insert,update,read"/>
<grant role="su" entity-name="User" actions="*"/>

このロール名は使用する JACC プロバイダに理解されるロールです。