IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  WebSphere | Open source  >

レガシー Hibernate アプリケーションから OpenJPA および EJB 3.0 へのマイグレーション

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません

原文はこちら

原文はこちら


レベル: 中級

Donald Vines (dhvines@us.ibm.com), Executive IT Architect, IBM 
Kevin Sutter (sutter@us.ibm.com), Senior Software Engineer, IBM

2007年 08月 22日

EJB 2.1 を使用した Hibernate アプリケーションの特徴と機能を OpenJPA および EJB 3.0 の対応する機能と比較することで、Hibernate アプリケーションのソース・コード、オブジェクト・リレーショナル・マッピング、そして構成パラメーターを OpenJPA にマイグレーションする方法を学んでください。IBM WebSphere 開発者向け技術ジャーナルより。

はじめに

Hibernate はオープン・ソースのパーシスタンス・フレームワーク兼クエリー・フレームワークで、POJO (Plain Old Java™ Object) とリレーショナル・データベース・テーブルとの間でオブジェクト・リレーショナル・マッピング (ORM) を行うとともに、クエリーを実行してデータを取得する機能を提供します。一方、EJB 3.0 Java Persistence API 仕様の定義に従って、POJO エンティティー用に同様のオープン・ソースのパーシスタンス・フレームワーク兼クエリー・フレームワークを提供するのが Apache OpenJPA プロジェクトです。この記事では EJB (Enterprise JavaBeans™) 2.1 を使用した一般的な Hibernate のシナリオを取り上げ、EJB 3.0 準拠の OpenJPA に実装された同等のシナリオと比較します。具体的に言うと、Hibernate アプリケーションのソース・コード、オブジェクト・リレーショナル・マッピング、そして構成パラメーターと、それに相当する OpenJPA のソース・コード、マッピング、そして構成との比較検討を行います。ここに記載する比較を見れば、マイグレーションにあたり、どのように変更すればいいかがわかるだけでなく、一般的なシナリオを使ったレガシー Hibernate アプリケーションから OpenJPA へのマイグレーションはそれほど難しくないことが理解できるはずです。

この記事で焦点とするのはレガシー Hibernate アプリケーションから OpenJPA へのマイグレーションですが、Hibernate に精通している読者にとっては、この新しい JPA 仕様を素早く使いこなし、OpenJPA 永続プロバイダーを今後のアプリケーション開発に使用する上でも有益な記事となります。

この記事では読者に Hibernate の基本概念についての知識があることを前提とし、特に Hibernate 3.0 実装に焦点を絞ります。記載するすべての例は、Hibernate 3 では EJB 2.1 を使用し、OpenJPA 0.9.7 では IBM® WebSphere® Application Server V6.1 Feature Pack for EJB 3.0 を使用して実行しています。

レガシー Hibernate アプリケーションを OpenJPA にマイグレーションする理由はさまざまにあります。例えば、Hibernate はオブジェクト・リレーショナル・マッピングと永続管理ソリューションの標準ではないなどといった理由です。Hibernate 3 には JDK 1.3.1 以降が必要です。それとは対照的に、OpenJPA が実装する JPA 仕様は Java 5 仕様の中核で、そして何よりも WebSphere Application Server V6.1 Feature Pack for EJB 3.0 実装のベースとなっています。これらの製品についての詳細は、「参考文献」を参照してください。

この記事のなかでは、JPA は仕様を指し、OpenJPA は JPA 仕様の実装を指します。

この記事では Hibernate のすべての特徴と機能を網羅することはしませんが、現場で頻繁に見られるベスト・プラクティスを取り上げます。




上に戻る


Hibernate アプリケーションのソース・コードのマイグレーション

JPA (Java Persistence API) は、Java コミュニティー全体で単一の標準パーシスタンス API を支持するために、EJB 3.0 仕様の一部として導入されました。JPA には、Hibernate、TopLink、Java Data Objects、そして EJB CMP (Container Managed Persistence) 2.1 仕様の選りすぐりの構想が駆使されています。

JPA は、Java SE (Java Platform, Standard Edition) 環境と Java EE (Enterprise Edition) 環境の両方で使用することができます。これは JPA ではエンティティーを、OpenJPA をはじめとする JPA 永続プロバイダーで管理可能な POJO として表現するためです。エンティティーのオブジェクト・リレーショナル・マッピングに関するメタデータは、Java 5 注釈を使って指定されることも、XML 記述子内に指定されることもあります。エンティティーは、Java オブジェクトをデータベースに対して存続させるために使用します。

JPA 永続プロバイダーは多数あります。IBM での JPA 仕様の実装は、Apache OpenJPA プロジェクトに基づいています。これらの JPA 永続プロバイダーがリリースされたことにより、カスタマーは現在、標準以外の永続プロバイダーとの非互換性を考えることなく標準 API に基づいてコーディングを行えるようになっています。

レガシー Hibernate アプリケーションから OpenJPA へのマイグレーションを支援するため、このセクションでは Hibernate で一般的に使用されている非標準 API を OpenJPA でこれに相当する標準 API と比較します。まず使用されているクラスとインターフェースを比較し、次に共通の使用シナリオで API を比較します。

このセクションでは、以下の領域ごとに詳細を説明します。

  1. クラスおよびインターフェース
  2. ランタイム構成
  3. セッション管理
  4. トランザクション管理
  5. エンティティー管理
  6. 分離エンティティー

1. クラスおよびインターフェース

通常は JPA 標準 API に基づいてコーディングを行うことになるので、この記事では JPA javax.persistence パッケージに焦点を絞り、OpenJPA 実装パッケージには重点を置きません。

以下の表は、一般的に使用される Hibernate のクラスとそれに対応する OpenJPA のクラスを比較したものです。Hibernate のクラスはすべて org.hibernate パッケージ内にあり、すべての JPA インターフェース (および Persistence クラス) は javax.persistence パッケージ内にあります。JPA インターフェースの OpenJPA 実装が含まれているのは org.apache.openjpa.* パッケージです。

org.hibernatejavax.persistence説明
cfg.ConfigurationPersistenceセッション・ファクトリー (Hibernate の場合) またはエンティティー・マネージャー・ファクトリー (OpenJPA の場合) を構成するブートストラップ・クラス。通常、JVM に単一のセッション (またはエンティティー・マネージャー) ファクトリーを作成するために使用されます。
SessionFactoryEntityManagerFactoryユーザー要求を処理する Hibernate セッション (または OpenJPA エンティティー・マネージャー) を開始するための API を提供します。セッション (またはエンティティー・マネージャー) は通常、クライアント要求を処理するスレッドごとに開始されます。
SessionEntityManagerデータベースに対してエンティティーを保管およびロードするための API を提供します。また、トランザクションの取得やクエリーの作成を行うための API も提供します。
TransactionEntityTransactionトランザクションの管理用 API を提供します。
QueryQueryクエリーを実行するための API を提供します。

2. ランタイム構成

  • Hibernate での規則

    Hibernate でのランタイム構成は、以下のようにマッピングされます。

    • 静的 SessionFactory 変数を使用します。
    • Configuration#configure() メソッドを使用します。
    • Configuration#buildSessionFactory() メソッドを使用します。


    リスト 1. Hibernate のランタイム構
                            
    public class ORMHelper {
    
      private static SessionFactory sf;
    
      protected static synchronized 
      SessionFactory getSessionFactory(String name) {
        if (sf == null) {
          sf = new Configuration().configure(name).buildSessionFactory();
        }
        return sf;
      }
      ...
    }

    一般的に、レガシー Hibernate アプリケーションには静的 SessionFactory インスタンスが 1 つあるだけで、このインスタンスが JVM 内でクライアント要求を処理するすべてのスレッドで共有されます。Hibernate では複数の SessionFactory インスタンスを作成することも可能ですが、実際に複数作成されることはほとんどありません。

    Hibernate で SessionFactory を構成する方法は何通りかありますが、最も一般的なシナリオは configure() メソッドを呼び出すという方法です。configure() に XML 構成ファイルの名前が渡されない場合、このメソッドはクラスパス上のルート・ディレクトリーで hibernate.cfg.xml を検索します。XML 構成ファイルの名前が渡されると、クラスパスでそのファイルを検索します。

    XML 構成ファイルが見つかると、buildSessionFactory() メソッドがその構成ファイルのメタデータを使用して SessionFactory を作成し、初期化します。

    注意すべき点は以下の点です。

    • 一部のアプリケーションでは静的変数を使用する代わりに JNDI レジストリーの SessionFactory を検索する場合があります。しかしこの場合も最初の検索では configure と buildSessionFactory を呼び出さなければならないため、この方法による利点はほとんどありません。したがって、静的変数を使う方が一般的です。
    • configure() メソッドを使ってファイルから Hibernate 構成パラメーターを読み取る代わりに、Configuration#setProperties() メソッドを使用し、プログラムによってパラメーターを構成することもできます。ただし、それよりも適切で頻繁に使用される手法は、Hibernate プロパティーを外部化することです。
Hibernate と同じく、OpenJPA でもエンティティー・マネージャー・ファクトリーを作成する際に追加プロパティーを渡すことができますが、XML ファイルでプロパティーを名前付きの永続ユニットに外部化するほうが一般的です。
  • OpenJPA での規則

    OpenJPA での相当するランタイム構成は、以下のようにマッピングされます。

    • 静的 EntityManagerFactory 変数を使用します。
    • Persistence#createEntityManagerFactory() を使用します。


    リスト 2. OpenJPA のランタイム構成
                            
    
    public class ORMHelper {
    
       private static EntityManagerFactory sf;
    
       protected static synchronized 
       EntityManagerFactory getSessionFactory(String name) { 
          if (sf == null) {
             sf = Persistence.createEntityManagerFactory(name);
          }
          return sf;
       }
       ...
    }

    Hibernate と同様、JVM でクライアント要求を処理するすべてのスレッドは単一の静的 EntityManagerFactory インスタンスを使用することができます。複数のインスタンスが必要な場合は、静的 Map を定義することも可能です。

    createEntityManagerFactory() はクラスパス上の META-INF フォルダーで、メソッド呼び出しで指定された名前と同じ名前を持つ永続ユニットが含まれる persistence.xml を検索します。指定された名前と一致する永続ユニットを持つ persistence.xml が見つかると、createEntityManagerFactory() がそのファイルのメタデータを使って EntityManagerFactory インスタンスを構成します。一致する名前を持つ persistence.xml が見つからない場合は、javax.persistence.PersistenceException が発生します。

3. セッション管理

一般に、アプリケーションはクライアント要求を受け取ると SessionFactory からセッションを取得し、要求の終了時にそのセッションを終了します。この場合、要求は HttpRequest であることも、ステートレス・セッション Bean の呼び出しなどであることもあります。セッションが提供するのは、トランザクションを処理し、データベースからエンティティーをロード (そしてエンティティーを保管) するためのメソッドです。

Hibernate アプリケーションは通常の場合、セッションを管理します。Hibernate アプリケーションがセッションを管理する際によく使う方法は、セッションをスレッドのローカル・ストレージに関連付け、そのセッションへのアクセスが必要なすべてのメソッドにセッションがパラメーターとして渡されないようにすることです。Hibernate 3.0.1 には getCurrentSession() も用意されていますが、たいていの場合は明示的なセッション管理が行われます。

例外に関しては、Hibernate 3.0 は非チェック例外またはランライム例外をスローします (OpenJPA 例外の場合も同じです)。つまり、ほとんどのアプリケーションはメソッド・シグニチャーの中では Hibernate 例外をスローしないため、アプリケーションのメソッドでは Hibernate 例外をキャッチすることも、処理することもありません。しかし Hibernate 例外をキャッチおよび処理したい場合には当然それも可能です。

さらに、既存のレガシー Hibernate アプリケーションの大部分は Java SE 1.4 を使用して実装されている一方、OpenJPA アプリケーションの実装には Java SE 5 を使用するのが一般的です。

以下の例では、getSessionFactory() ヘルパー・メソッドを使用して、セッション (またはエンティティー・マネージャー) の作成/開始に必要なセッション・ファクトリー (またはエンティティー・マネージャー・ファクトリー) を取得しています (getSessionFactory() メソッドの詳細は、「ランタイム構成」を参照)。

  • Hibernate での規則

    Hibernate でのセッション管理は、以下のようにマッピングされます。

    • ThreadLocal を使用して現行セッションを取得します。
    • SessionFactory#openSession() を使用してセッションを開始します。
    • Session#isOpen() および Session#close() を使用してセッションを終了します。


    リスト 3. Hibernate のセッション管理
                            
    public class ORMHelper {
    
       private static final ThreadLocal tls = new ThreadLocal();
    
       public static void openSession() {
          Session s = (Session) tls.get();
          if (s == null) {
             s = getSessionFactory("test.cfg.xml").openSession();
             tls.set(s);
          }
       }
    
       public static Session getCurrentSession() {
          return (Session) tls.get();
       }
    
       public static void closeSession() {
          Session s = (Session)tls.get();
          tls.set(null);
          if (s != null && s.isOpen()) s.close();
       }
       ...
    }

  • OpenJPA での規則

    OpenJPA での相当する EntityManager 管理は、以下のようにマッピングされます。

    • ThreadLocal を使用して現行のエンティティー・マネージャーを取得します。
    • EntityManagerFactory#createEntityManager() を使用してセッションを開始します。
    • EntityManager#isOpen() および EntityManager#close() を使用してセッションを終了します。


    リスト 4. OpenJPA のセッション管理
                            
    public class ORMHelper{
    
       private static final ThreadLocal tls = new ThreadLocal();
    
       public static void openSession() {
          EntityManager s = (EntityManager) tls.get();
          if (s == null) {
             s = getSessionFactory("test").createEntityManager();
             tls.set(s);
          }
       }  
    
       public static EntityManager getCurrentSession() {
          return (EntityManager) tls.get();
       }
    
       public static void closeSession() {
          EntityManager s = (EntityManager) tls.get();
          tls.set(null);
          if (s != null && s.isOpen()) s.close();
       }
       ...
    }

4. トランザクション管理

Hibernate アプリケーションは各種のトランザクション手法を使用したさまざまな環境で動作することができます。アプリケーションは環境内で、ローカル JDBC あるいはグローバル JTA (Java Transaction API) トランザクションを使用して動作します。

最も一般的なシナリオは、ローカル JDBC トランザクションを使用することです。JTA トランザクションは、データベースとメッセージ・キューなどといった異種のデータ・ストアがある場合に役立ちます。JTA を使うとデータ・ストアを単一のトランザクションとして扱うことができるためです。

Hibernate アプリケーションではトランザクション API を呼び出してアプリケーション固有のトランザクションを管理します。使用するトランザクション手法 (JDBC または JTA) は Hibernate 構成ファイルに設定されるので、アプリケーションにとってはどちらの手法が使用されていようと関与するところではありません。

  • Hibernate での規則

    Hibernate でのトランザクション管理は、以下のようにマッピングされます。

    • Session#beginTransaction() を使用してトランザクションを開始します。
    • Transaction#commit() を使用してトランザクションをコミットします。
    • Transaction#isActive および Transaction#rollback() を使用してトランザクションをロールバックします。


    リスト 5. Hibernate のトランザクション管理
                            
    public class ORMHelper {
    
       private static final ThreadLocal tltx = new ThreadLocal();
    
       public static void beginTransaction() {
          Transaction tx = (Transaction) tltx.get();
          if (tx == null) {
             tx = getCurrentSession().beginTransaction();
             tltx.set(tx);
          }
       }
    
       public static void commitTransaction() {
          Transaction tx = (Transaction)tltx.get();
          if (tx != null && tx.isActive()) tx.commit();
          tltx.set(null);
       }
    
       public static void rollbackTransaction() {
          Transaction tx = (Transaction)tltx.get();
          tltx.set(null);
          if (tx != null && tx.isActive()) tx.rollback(); 
       }
       ...
    }

  • OpenJPA での規則

    OpenJPA での相当するトランザクション管理は、以下のようにマッピングされます。

    • EntityManager#getTransaction() および EntityTransaction#begin() を使用します。
    • EntityTransaction#commit() を使用します。
    • EntityTransaction#isActive() および EntityTransaction#rollback() を使用します。


    リスト 6. OpenJPA のトランザクション管理
                            
    public class ORMHelper {
    
       private static final ThreadLocal tltx = new ThreadLocal();
    
       public static void beginTransaction() {
          EntityTransaction tx = (EntityTransaction) tltx.get();
          if (tx == null) {
             tx = getCurrentSession().getTransaction();
             tx.begin();
             tltx.set(tx);
          }
       }
    
       public static void commitTransaction() {
          EntityTransaction tx = (EntityTransaction)tltx.get();
          if (tx != null && tx.isActive()) tx.commit();
          tltx.set(null);
       }
    
       public static void rollbackTransaction() {
          EntityTransaction tx = (EntityTransaction)tltx.get();
          tltx.set(null);
          if (tx != null && tx.isActive()) tx.rollback(); 
       }
       ...
    }

    上記の OpenJPA の例ではトランザクションを ThreadLocal に保管していますが、一般的には単に getCurrentSession().getTransaction().begin() を呼び出し、後から getCurrentSession().getTransaction().commit() を呼び出します。したがって、OpenJPA ではトランザクションを ThreadLocal に保管する必要は実際にはありません。

5. エンティティー管理

エンティティー管理の一般的なシナリオは以下のとおりです。

  • 永続オブジェクトを作成する
  • 主キーを使用して永続オブジェクトを取得する
  • 永続オブジェクトを更新する
  • 永続オブジェクトを削除する

通常、上記のシナリオは個々のユース・ケースの動作にマッピングされるため、個別のトランザクションで分離エンティティーを使って実行されますが、接続されたエンティティーでも使用することができます。

  • Hibernate での規則

    Hibernate でのエンティティー管理は、以下のようにマッピングされます。

    • Session#save を使用して一時オブジェクトを永続オブジェクトに変更します。
    • Session#load を使用して永続オブジェクトを取得します。
    • Session#update を使用して分離オブジェクトの永続状態を更新します (永続 (接続) オブジェクトの状態を変更する場合、update を呼び出す必要はありません。Hibernate は、commit() が呼び出されると自動的に更新をデータベースに伝播するためです)。
    • Session#delete を使用して分離オブジェクトまたは永続オブジェクトを一時オブジェクトに変更します。


    リスト 7. Hibernate のエンティティー管理
                            
    public class ORMHelper {
       ...
       public static void create(Serializable obj) {
          getCurrentSession().save(obj);		    
       }
    
       public static Object retrieve(Class clz, Serializable key) {
          return getCurrentSession().load(clz, key);
       }
    	
       public static void update(Serializable obj ) {
          getCurrentSession().saveOrUpdate(obj);
       } 
    
       public static void delete(Serializable obj ) {
          getCurrentSession().delete(obj);
       }
    }

  • OpenJPA での規則

    OpenJPA での相当するエンティティー管理は、以下のようにマッピングされます。

    • EntityManager#persist を使用して永続エンティティーを作成します。
    • EntityManager#find を使用して永続エンティティーを取得します。
    • EntityManager#merge を使用して分離エンティティーを更新します ((接続された) 永続エンティティーを操作している場合、merge を呼び出す必要はありません。OpenJPA は、トランザクションの終了時に更新をデータベースに伝播するためです)。
    • EntityManager#remove を使用して分離エンティティーまたは永続エンティティーを削除します。


    リスト 8. OpenJPA のエンティティー管理
                            
    public class ORMHelper {
       ...
       public static void create( Serializable obj ) {
          getCurrentSession().persist((Object) obj);		    
       }
    
       public static Object retrieve(Class clz, Serializable key) {
          return getCurrentSession().find( clz, (Object) key );
       }
    	
       public static Object update( Serializable obj ) {
          return getCurrentSession().merge((Object) obj);
       } 
    
       public static void delete( Serializable obj ) {
          getCurrentSession().remove( (Object) obj);
       } 
    }

    上記の例では、ORMHelper#update() メソッドのシグニチャーが変更されています。これは、Hibernate の update() メソッドが新しく管理対象となった永続状態を、そのメソッドに渡された分離 (または一時) オブジェクトにコピーするためです。したがって戻り値はありません。それとは逆に、OpenJPA の merge() メソッドでは元の分離 (一時) オブジェクトは変更されないため、戻り値に新しく管理対象となった永続状態が含まれることになります。

    ORMHelper#update シグニチャーの変更に加え、ORMHelper#update を呼び出していたレガシー・アプリケーションも、元の一時オブジェクトに明示的に戻り値を割り当てるように変更する必要がありました。

6. 分離エンティティー

階層化アーキテクチャーに基づく Hibernate アプリケーションで共通のシナリオには、ステートレス・セッション EJB をセッション・ファサードとして使用して、「分離された」エンティティーを Web 層に返すというものもあります。このシナリオでは Web 層からの呼び出しに応じて、セッション EJB がトランザクションを開始および終了します。

階層化アーキテクチャーに対するこのようなパターンのメリットは、それぞれのユーザー操作ごとに EJB 層がトランザクションを開始および終了するため、ユーザーが何らかの作業を行っている間、トランザクションが開始された状態のままにならないという点です。したがって、すべてのトランザクションは短時間しか存続せず、数秒のうちに完了します。

既存の Hibernate アプリケーションでは、EJB 2.1 を使用してセッション EJB を実装するのが通例ですが、ほとんどの OpenJPA アプリケーションでは EJB 3.0 を使用することになります。まず始めに、リソースのローカル・エンティティー・マネージャーを使って EJB 3.0 セッション Bean にマイグレーションしてください。こうすれば、トランザクション・ロジックを変更する必要がないだけでなく、EJB 2.1 セッション Bean から OpenJPA を使用することも可能になります (「Leveraging OpenJPA with WebSphere Application Server V6.1」を参照)。このマイグレーションが完了したら、今度は JTA エンティティー・マネージャーを使用してアプリケーションを EJB 3.0 にリファクタリングするかどうかを検討する必要があります。

分離エンティティーに関してもう 1 つ注意する点として、トランザクションでオブジェクトを取得した後、分離オブジェクトをそのトランザクションの外で変更した場合には、該当するオブジェクトで update を呼び出してデータベースにオブジェクトを保存し直さなければなりません。これは、Hibernate だけでなく OpenJPA でも一般的なプログラミングの慣例です。同様に、オブジェクトを取得してから同じトランザクション内でそのオブジェクトを変更した場合には、そのオブジェクトに関して update を呼び出してデータベースに保存する必要はありません。トランザクションをコミットすれば、そのオブジェクトは自動的にデータベースに書き込まれます。これも同じく、Hibernate と OpenJPA での一般的なプログラミングの慣例です。

  • Hibernate での規則

    Hibernate での EJB 2.1 を使用した分離エンティティーは、以下のようにマッピングされます。

    • セッション・ファサード・パターンを使用してエンティティーをラップします。
    • 分離エンティティー (POJO) を Web 層に返します。


    リスト 9. EJB 2.1 を使用した Hibernate の分離エンティティー
                            
    public class CustomerFacadeBean implements SessionBean, CustomerFacade{
    	
      public Customer createCustomer( Customer customerEntity ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          ORMHelper.create(customerEntity);
          ORMHelper.commitTransaction();
          return customerEntity;
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
      public Customer updateCustomer( Customer customerEntity ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          ORMHelper.update(customerEntity);
          ORMHelper.commitTransaction();
          return customerEntity;
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
      public Customer getCustomer( Long customerId ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          Customer customerEntity;
          customerEntity = ORMHelper.retrieve(Customer.class,customerId);
          ORMHelper.commitTransaction();
          return customerEntity;
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
      public void deleteCustomer( Customer customerEntity ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          ORMHelper.delete(customerEntity);
          ORMHelper.commitTransaction();
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          Throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
      ...   
    }

  • OpenJPA での規則

    OpenJPA での EJB 3.0 を使用した分離エンティティーは、以下のようにマッピングされます。

    • セッション・ファサード・パターンを使用してエンティティーをラップします。
    • 分離エンティティー (POJO) を Web 層に返します。


    リスト 10. EJB 3.0 を使用した OpenJPA の分離エンティティー
                            
    @Stateless 
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public class CustomerFacadeBean implements CustomerFacade {
    
      public Customer createCustomer( Customer customerEntity ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          ORMHelper.create(customerEntity);
          ORMHelper.commitTransaction();
          return customerEntity;
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
      public Customer updateCustomer( Customer customerEntity ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          Customer returnValue;
          returnValue = ORMHelper.update(customerEntity);
          ORMHelper.commitTransaction();
          return returnValue;
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
      public Customer getCustomer( Long customerId ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          Customer customerEntity;
          customerEntity = ORMHelper.retrieve(Customer.class,customerId);
          ORMHelper.commitTransaction();
          return customerEntity;
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
      public void deleteCustomer( Customer customerEntity ) {
        ORMHelper.openSession();
        try {
          ORMHelper.beginTransaction();
          ORMHelper.delete(customerEntity);
          ORMHelper.commitTransaction();
        } catch (RuntimeException ex) {
          ORMHelper.rollbackTransaction();
          throw ex;
        } finally {
          ORMHelper.closeSession();
        }
      }
    
    }

    リスト 10 を見ると、マイグレーションする上で必要だった変更がわかります。

    リストを比較すると、EJB 3.0 の SessionFacade クラスは SessionBean クラスも、SessionBean のコールバック・メソッド (setSessionContext、ejbCreate、ejbRemove、ejbActivate、および ejbPassivate) も実装していません。さらに、 EJB 3.0 ではコンポーネント・インターフェース、ホーム・インターフェース、そしてデプロイメント記述子も不要です。EJB 2.1 デプロイメント記述子に指定された値は、Java 5 注釈によって EJB 3.0 の SessionFacade クラスに組み込まれます。

    それにも関わらず、Hibernate または OpenJPA の API に対する呼び出しの大半があると思われる SessionFacade の必要とされる機構のメソッドは一切変更されていません。その理由は、私たちは Hibernate/OpenJPA のほとんどの API をヘルパー・クラス内にカプセル化したためで、セッション Bean はこのヘルパー・クラスを使用しています。お使いのアプリケーションが Hibernate/OpenJPA の API をヘルパー・クラス内にカプセル化するように構成されていないことも当然考えられますが、これまでのセクションに記載したヘルパー・クラスに対する変更と見比べると、セッション、トランザクション、そしてエンティティーを管理するために必要な EJB セッション Bean の変更が判断できるはずです。




上に戻る


Hibernate のオブジェクト・リレーショナル・マッピングのマイグレーション

Hibernate のオブジェクト・リレーショナル・マッピングは、起動時にロードされる一連の XML マッピング・ファイルの中で定義することができます。これらのマッピング・ファイルは直接使用することも、ソース・コードに組み込まれた javadoc スタイルの注釈から生成することもできます。また、Hibernate の最新バージョンでは Java 5 注釈によってオブジェクト・リレーショナル・マッピングを定義することも可能になっています。

OpenJPA のオブジェクト・リレーショナル・マッピングを定義するには、一連のマッピング・ファイルを使用するか、あるいは Java 5 注釈をコードに直接埋め込みます。後者の場合、マッピング・ファイルは完全に不要になります。

大抵のレガシー Hibernate アプリケーションでは XML マッピング・ファイルを使用する一方、OpenJPA アプリケーションの大半では、開発時に Java 5 注釈を使用し、実動時に XML に移行することで、マッピングに対する単純な変更を行うためにソース・コードを変更してリビルドしなくても済むようにしています。

XML は Hibernate では一般的であり、OpenJPA でも実動用には通常 XML を使用するので、このセクションでは XML の場合のマッピングについて記載します。オブジェクト・モデル (POJO) に必要 (または不要) な内容を理解しやすいように、対応する基本コード (注釈は除く) も併記します。

お使いのレガシー Hibernate アプリケーションでマッピング・ファイルを使用していないとしても (javadoc スタイルの注釈や Java 5 注釈を使用しているなどの場合)、このセクションの情報を基に、そのアプリケーションを OpenJPA にマイグレーションするための変更を割り出せるはずです。逆に OpenJPA で Java 5 注釈を使用したいという読者は、「付録」に記載した例を参照してください。

Java オブジェクトとリレーショナル・テーブルの間のマッピングには、さまざまな方法があります。このセクションでは、エンタープライズ・アプリケーションに見られる以下の一般的な方法について説明します。

  1. 継承
  2. 関係
  3. 遅延初期化
  4. オブジェクト ID
  5. オプティミスティック・ロック

1. 継承

エンタープライズ・アプリケーションのデータ・モデルには通常、クラスの汎化/特化によって有効な再利用が可能な箇所がいくつかあります。Hibernate と OpenJPA ではいずれも、3 通りの方法でリレーショナル・テーブルの中で継承をモデル化することができます。ここではこの 3 つの方法のうち、私たちが一般的な方法だと考える以下の 2 つの方法について説明します。

  1. 単一のテーブルによる継承
  2. 結合による継承

3 つ目の方法 (具象クラスごとのテーブル) は使用されることは少ないものの、Hibernate によって提供されている方法です。また、OpenJPA などの JPA 永続プロバイダーではオプションの実装となっています。

a. 単一のテーブルによる継承

Java 基本クラスにすべてのサブクラスに対する属性のほとんどが含まれる場合、弁別子列を持つ単一のテーブルで継承をマッピングすることができます。弁別子列の値が識別するのは、行に示されたインスタンスが属する特定のサブクラスです。特定のサブクラスにマッピングされない列は該当するサブクラスのデータベース列では空になるため、これらの列については NULL 可能な列として宣言しなければなりません。

この継承による手法の欠点は、該当するインスタンスに対して NULL にはできない複数のプロパティーをサブクラスが定義すると、非 NULL の制約が失われてしまうため、データ保全性の問題が生じることです。一方、複雑な結合がないため、エンティティーとクラス階層全体を範囲とするクエリーとの間のポリモーフィックな関係に対して最善のサポートを提供するという大きな利点があります。

  • オブジェクト・モデル

    マッピング 1. 単一のテーブルによる継承 (POJO)
                            
    // Participant (base) class
    public class Participant implements Serializable{ 
        private Long participantId;
        ... 
    }
    
    // SalesRep (sub) class
    public class SalesRep extends Participant { ... }
    
    // Administrator (sub) class
    public class Administrator extends Participant { ... }

  • Hibernate での規則

    Hibernate での単一のテーブルによる継承は、以下のようにマッピングされます。

    • テーブルにマッピングされた基本クラスでは、弁別子列を指定した class を使用します。主キーやその他のプロパティーのマッピングを定義することもできます (これについては後で説明するので、以下の例には記載していません)。
    • サブクラスでは、固有の弁別子の値を指定した subclass を使用します。サブクラス固有のプロパティーのマッピングを定義することもできます。サブクラスには ID 要素を定義しません。サブクラスには固有のテーブルがないため、(基本) クラスの ID を使用します。


    マッピング 2. 単一のテーブルによる継承 (Hibernate の XML マッピング)
                            
    <!-- Participant (base) class -->
    <class name="Participant" table="T_PRTCPNT" >
       <id name="participantId" column="PRTCPNT_TID"/>
       <discriminator column="PRTCPNT_TYPE" type="string"/>
       ...
    </class>
    
    <!-- SalesRep subclass -->
    <subclass 
      name="SalesRep"
      extends="Participant" 
      discriminator-value="SALES_REP">
      ...
    </subclass>
    
    <!-- Administrator subclass -->
    <subclass name="Administrator"
       extends="Participant" 
       discriminator-value="ADMIN">
       ...
    </subclass>

  • OpenJPA での規則

    OpenJPA での単一のテーブルによる継承は、以下のようにマッピングされます。

    • 基本クラスでは、SINGLE_TABLE 継承による手法と弁別子列を使用します。基本クラスの永続プロパティーとその固有 ID を定義することもできます。
    • サブクラスでは、弁別子の値を使用してサブクラスのインスタンスを表します。サブクラスの永続プロパティーを定義することもできますが、ID は定義できません。サブクラスにはテーブルがないためです。サブクラスのプロパティーは、基本クラスを示すテーブルにプロモートされます。

      マッピング 3. 単一のテーブルによる継承 (OpenJPA の XML マッピング)
                                      
      <entity class="Participant">
         <table name="T_PRTCPNT"/>
         <inheritance strategy="SINGLE_TABLE"/>
         <discriminator-column name="PRTCPNT_CLASS"/>
      
         <attributes>
            <id name="participantId">
               <column name="PRTCPNT_TID"/>
            </id>
            ...
         </attributes>
      </entity>
      
      // SalesRep subclass
      <entity class="SalesRep">
         <discriminator-value>SALES_REP</discriminator-value>
         ...
      </entity>
      
      
      // Administrator subclass
      <entity class="Administrator">
         <discriminator-value>ADMIN</discriminator-value>
         ...
      </entity>

b. 結合による継承

基本クラスにすべてのサブクラスに対する属性のほとんどが含まれていない場合、基本クラスの属性が含まれる 1 つのテーブルと各サブクラスに対応する個別の結合テーブルを併用します。この結合テーブルには、非継承プロパティーの列しか含まれないため、サブクラスのインスタンスを読み取るには基本クラス・テーブルとサブクラス・テーブルとを結合しなければなりません。

結合による継承の手法には、NULL 以外のプロパティーをサブクラスに定義できるという利点がある一方、インスタンスを構成するために複数の結合が必要になるという欠点もあります。しかしこの手法は、基本クラス・テーブルを変更せずに新しいサブクラスを定義したり、既存のサブクラスにプロパティーを追加できるという点で柔軟性にはひときわ優れています。

  • オブジェクト・モデル

    マッピング 4. 結合による継承 (POJO)
                            
    // Participant (base) class
    public class Participant implements Serializable { 
        private Long participantId;
        ... 
    }
    
    // SalesRep (sub) class
    public class SalesRep extends Participant { 
        ... 
    }
    
    // Administrator (sub) class
    public class Administrator extends Participant { 
        ... 
    }

  • Hibernate での規則

    Hibernate での結合による継承は、以下のようにマッピングされます。

    • 基本クラスでは、主キー (id) を指定した class 要素を使用します。基本クラスを構成するプロパティーのマッピングを定義することもできます。
    • サブクラスでは、外部キー列に基本クラスの主キーが含まれる結合サブクラス (joined-subclass) を使用します。結合サブクラスにはその他のプロパティーのマッピングを定義することもできます (以下の例には記載していません)。


    マッピング 5. 結合による継承 (Hibernate の XML マッピング)
                            
    <!-- Participant (base) class -->
    <class name="Participant" table="T_PRTCPNT" >
       <id name="participantId" column="PRTCPNT_TID"/>
       ...
    </class>
    
    <!-- SalesRep joined-subclass -->
    <joined-subclass 
       name="SalesRep" 
       extends="Participant" 
       table="T_SALESREP">
       <key column="PRTCPNT_TID"/>
       ...
    </joined-subclass>
    
    <!-- Administrator joined-subclass -->
    <joined-subclass 
       name="Administrator" 
       extends="Participant" 
       table="T_ADMIN">
       <key column="PRTCPNT_TID"/>
       ...
    </joined-subclass>

  • OpenJPA での規則

    OpenJPA での結合による継承は、以下のようにマッピングされます。

    • 基本クラスでは、JOINED 継承の手法を使用します。基本クラスはすべての結合サブクラスが使用する主キー、そしてオプションでバージョン列も定義することも可能です。また基本クラス・プロパティーのマッピングを定義することも可能です。
    • サブクラスでは、サブクラスの永続プロパティーを定義します。サブクラス・テーブルには、これらの永続プロパティーが含まれ、基本クラスの主キーに結合するための外部キーとして使用される主キー列があります。サブクラスはバージョン列を定義しません。


    マッピング 6. 結合による継承 (OpenJPA の XML マッピング)
                            
    <!-- Participant (base) class -->
    <entity class="Participant">
       <table name="T_PRTCPNT"/>
       <inheritance strategy="JOINED"/>
       <attributes>
          <id name="participantId">
             <column name="PRTCPNT_TID"/>
          </id>
          ...
       </attributes
    </entity>
    
    
    <!-- SalesRep subclass -->
    <entity class="SalesRep">
       <table name="T_SALESREP"/>
       <primary-key-join-column name="PRTCPNT_TID"/>
       ...
    </entity>
    
    <!?Administrator subclass -->
    <entity class="Administrator">
       <table name="T_ADMIN"/>
       <primary-key-join-column name="PRTCPNT_TID"/>
       ...
    </entity>

2. 関係

オブジェクト・モデルのオブジェクト (およびデータ・モデルのテーブル) にはさまざまな相互関係が必要です。データ・モデルでは同様のデータ・クラスの列間の関係は明示されませんが、オブジェクト・モデルはオブジェクト間の関係について明示的である必要があります。これは、トラバーサルをサポートするためです。さらに、データ・モデルの関係には本質的に方向性がありませんが (ただし、一方向での検索のほうが効率的な場合もあります) オブジェクト・モデルの関係には本質的にオブジェクト間の方向性があります。

オブジェクト・モデルの関係は、あるテーブルの外部キーが別のテーブルの主キーを参照するという形でデータ・モデルに実装されます。外部キーを持つテーブルは子オブジェクトと呼ばれます。子オブジェクトの行が示すオブジェクトの存続時間はその参照先のオブジェクトに依存するため、このテーブルには親オブジェクトへの外部キーが含まれます。子オブジェクトには外部キーがあるため、子オブジェクトは孤児にならないよう常に有効な親オブジェクトを指す必要があります。つまり、親オブジェクトを削除するには、まずその親オブジェクトの子オブジェクトを削除するか、親から子への削除をカスケードしなければなりません。

関係の管理では、子オブジェクトは関係を所有する側、親オブジェクトは所有される側とも呼ばれます。この「所有する側」という概念が重要になる理由は、Java プログラマーは双方向の関係の両側を設定しなければなりませんが、データベースは一方の値のみを更新して変更を反映させるためです。つまり、更新は子オブジェクト (つまり、所有する側) の外部キーに対して行われます。したがって、子オブジェクトの外部キーを示すプロパティーの変更はデータベースには伝播されますが、親オブジェクトの inverse プロパティーの変更はデータベースには伝播されません。

関係のマッピングは、以下の 4 つに分類できます。

  1. 1 対 1
  2. 多対 1
  3. 1 対多
  4. 多対

a. 1 対 1 の関係

1 対 1 の関係が定義するのは、別の永続オブジェクトへの参照です。この関係では、子オブジェクトの存続時間は完全に親オブジェクトの存続時間に依存します。

コンポーネント・オブジェクトとの 1 対 1 の関係をモデル化すると、オブジェクト・モデルでは 2 つの別個のクラスになりますが、データ・モデルで 2 つの別のテーブルになることはありません。

1 対 1 の関係は例外的なケースです。この関係が生じた場合、Hibernate では一般的に関係をコンポーネント・オブジェクトとしてモデル化します。子オブジェクトのすべてのプロパティーが親オブジェクトのテーブルに適合するようにすることで、親と子を結合しなくても子のプロパティーにアクセスできるようにするためです。

Hibernate では上記の他、以下の 2 つの方法で 1 対 1 の関係をモデル化できます。

このセクションの残りでは、Hibernate のコンポーネント・オブジェクトを使用した 1 対 1 の関係についてのマイグレーションを取り上げ、これらの関係を OpenJPA の組み込み可能オブジェクトにマイグレーションする方法を説明します。

  • オブジェクト・モデル


    マッピング 7. 1 対 1 の関係 (POJO)
                            
    // Employee (parent) class
    public class Employee implements Serializable {
    
        private Long employeeId;
        private EmployeeRecord employeeRec;
        ...
    }
    
    // EmployeeRecord (child) class
    public class EmployeeRecord implements Serializable { ... }

  • Hibernate での規則

    Hibernate でのコンポーネント・オブジェクトを使用した 1 対 1 の関係は、以下のようにマッピングされます。

    • class を使用して親クラスをモデル化します。親の内部には、主キー (id) やその他の親のプロパティーも定義することができます。
    • ネストした component 要素を使用して子クラスをモデル化します。ネストした component には、必要に応じて追加プロパティーも定義することができます。


    マッピング 8. コンポーネント・オブジェクトを使用した 1 対 1 の関係 (Hibernate の XML マッピング)
                            
    <!?Employee (parent) class
    <class name="Employee" table="T_EMPLOYEE">
        ...
        <id name="employeeId" column="EMP_TID"/>
    
        <!-- EmployeeRecord (child) class
        <component name="employeeRec" class="EmployeeRecord">
            ...
        </component>
    </class>

  • OpenJPA での規則

    OpenJPA での組み込み可能オブジェクトを使用した 1 対 1 の関係は、以下のようにマッピングされます。

    • 親エンティティー (Employee など) に埋め込みフィールドを宣言します。埋め込みフィールドは子エンティティーに対する関係となるのではなく、親エンティティーのデータベース・レコードの一部としてマッピングされて親エンティティーに埋め込まれます。
    • 子エンティティー (EmployeeRecord など) を埋め込み可能として定義します。

      マッピング 9. 埋め込み可能オブジェクトを使用した 1 対 1 の関係 (OpenJPA の XML マッピング)
                                      
      <!-- Employee (parent) class -->
      <entity class="Employee">
          <table name="T_EMPLOYEE"/>
         <attributes>
            <id name="employeeId">
               <column name="EMP_TID"/>
            </id>
            <embedded name="employeeRec"/>
          ...
          </attributes>
      </entity>
      
      <!-- EmployeeRecord (child) class -->
      <embeddable class="EmployeeRecord">
         ...
      </embeddable>

b. 多対 1 の関係

多対 1 の関係が定義するのは、単一の永続オブジェクトへの参照です。多対 1 の関係は単一方向にすることも可能ですが、たいていは 1 対多の双方向関係の逆として定義されます。

多対 1 の関係を宣言するエンティティーのテーブルには外部キーがあるため、このエンティティーが子オブジェクト (関係を所有する側) となります。一方、多対 1 の関係を宣言するエンティティーによって参照されるオブジェクトは親オブジェクトです。親オブジェクトのテーブルには外部キーがないため、親オブジェクトは所有される側、つまり関係の逆側となります。

  • オブジェクト・モデル


    マッピング 10. 多対 1 の関係 (POJO)
                            
    // Address (parent) class
    
    public class Address implements Serializable {
        private Long addressId;
      ...
    }
    
    // Phone (child) class
    public class Phone implements Serializable {
        private Address address;
        ...
    }

  • Hibernate での規則

    Hibernate での多対 1 の関係は、以下のようにマッピングされます。

    • 子クラスでは many-to-one 要素を使用します。
    • 親クラスでは主キーを定義します。


    マッピング 11. 多対 1 の関係 (Hibernate の XML マッピング)
                            
    <!-- Phone (child) class -->
    <class name="Phone" table="T_PHONE">
        <many-to-one 
            name="address" 
            class="Address" 
            column="ADDR_TID">
        </many-to-one>
        ...
    </class>
    
    <!-- Address (parent) class -->
    <class name="Address" table="T_ADDRESS">
        <id name="addressId" column="ADDR_TID"/>
        ...
    </class>

  • OpenJPA での規則

    OpenJPA での多対 1 の関係は、以下のようにマッピングされます。

    • 親エンティティーでは主キー (id) を定義します。
    • 子エンティティーでは、many-to-one 要素を使用して関係を定義し、ネストした join-column 要素を使用して外部キーを定義します。join-column が指定するのは、結合によって子の親エンティティーを検索する際の手段です。


    マッピング 12. 多対 1 の関係 (OpenJPA の XML マッピング)
                            
    <!-- Address (parent) class -->
    <entity class="Address">
       <table name="T_ADDRESS"/>
       <attributes>
          <id name="addressId">
             <column name="ADDR_TID"/>
          </id>
          ...
       </attributes>
    </entity>
    
    <!-- Phone (child) class -->
    <entity class="Child">
       <table name="T_PHONE"/>
       <attributes>
          <many-to-one name="address">
             <join-column name="ADDR_TID"/>
          </many-to-one>
          ...
       </attributes>
    </entity>

c. 1 対多の関係

1 対多の関係は、オブジェクトのコレクションへの参照を定義します。この関係は、オブジェクト・モデルでは最も一般的なものです。ユース・ケースでは一般的に親オブジェクトから子オブジェクトへのトラバーサルが必要ですが、子から親へのトラバーサルは必ずしも必要ではありません。つまり、ほとんどの場合は一方向の 1 対多の関係で十分だということです。

ただし子から親へのトラバーサルが必要なユース・ケースでも、子クラスに多対 1 の関係を追加することで、簡単に関係を双方向にすることができます。

親オブジェクト (所有される側) となるのは、1 対多の関係を宣言するエンティティーです。このエンティティーのテーブルは主キーを定義しますが、外部キーはこのテーブルにありません。外部キーは子オブジェクトにあります。

1 対多の関係で参照されるオブジェクトは子オブジェクトで、関係を所有する側です。子オブジェクトは外部キーを持ち、親オブジェクトの主キーを参照します。

Hibernate では通常、子のテーブルに外部キーの列を追加して 1 対多の関係をマッピングしますが、マッピングの詳細は、関係が一方向か双方向かによって異なります。

一方向の場合、子のテーブルに含まれる外部キー列は子オブジェクトのプロパティーにマッピングされません。プロパティーへのマッピングはオブジェクト・モデルではなく、データ・モデルで行われます。一方向であることから、プロパティーは親オブジェクトにだけ存在し、子オブジェクトには存在しません。さらに、外部キー列は NULL 可能として定義する必要があります。Hibernate では最初に子の行 (NULL 外部キーを指定) を挿入し、後でこの行を更新するためです。

双方向の場合は、オブジェクト・リレーショナル・マッピングのほうが適しています。子オブジェクトには外部キー列のプロパティーがあり、この列はデータベース内で NULL 可能でなくても構わないためです。ただし、結果的なオブジェクト・モデルはオブジェクト間の循環依存関係と一層の密結合を持つことになるため、関係の両側を設定するための追加プログラミングが必要になります。

以上のように、1 対多の関係を定義する上ではいくつかのトレードオフを考慮しなければなりませんが、ユース・ケースで双方向のナビゲーションが必要でない限り、通常は一方向の関係が推奨されます。

  • オブジェクト・モデル


    マッピング 13. 1 対多の関係 (POJO)
                            
    // Address (parent) entity
    public class Address implements Serializable {
        private Long addressId;
        private Set phones = new HashSet();
        ... 
    }        
    
    // Phone (child) entity
    public class Phone implements Serializable {
        ...
    }

  • Hibernate での規則

    Hibernate での 1 対多の関係 (一方向) は、以下のようにマッピングされます。

    • 親クラスでは、one-to-many サブ要素を指定した set、bag、または list を使用します。
    • 関係が一方向の場合は、子クラスを示すテーブルで外部キーを作成します。双方向の関係には多対 1 の関係を使用します。


    マッピング 14. 1 対多の関係 (Hibernate の XML マッピング)
                            
    <!-- Address (parent) class -->
    <class name="Address" table="T_ADDRESS">
       <id name="addressId" column="ADDR_TID"/>
    
       <set
          name="phones"                 
          table="T_PHONE"
          cascade="all-delete-orphan">
          <key column="ADDR_TID"/>
          <one-to-many class="Phone">
       </set>
    
    </class>
    
    <!-- Phone (child) class -->
    <class name="Phone" table="T_PHONE">
       ...
    </class>

    重要な点として、Hibernate 独自の機能、cascade="all-delete-orphan" に注目してください (マッピング 14 を参照)。この属性を使用すると、子を親のコレクションから削除してから親をデータベースに保存するだけで、データベースから子を削除することができます。この独自の機能では、コードでの明示的な子の削除はありません。これに相当する機能は標準 JPA にはありませんが、OpenJPA には自動で孤児をクリーンアップする独自の注釈、@ElementDependent があります。ただし、この機能は他の永続プロバイダーには移植できないため、コードを理解する上で混乱の種となる可能性があります。all-delete-orphan 機能については後で詳しく説明します。

  • OpenJPA での規則

    OpenJPA では、一方向の 1 対多の関係は外部キーを使ってマッピングすることができないため、結合テーブルを使用してマッピングしなければなりません。ただし関係が双方向であれば外部キーを使用してマッピングすることができます。したがって、Hibernate による一方向の 1 対多の関係をマッピングする方法は以下の 2 通りとなります。

    1. 結合テーブルを使用してマッピングし、結合テーブルをデータベースに追加します。
    2. 外部キーを使用してマッピングしてから関係を一方向から双方向に変換し、inverse プロパティーをオブジェクト・モデルに追加して、関係が双方向に設定されるようにコードを変更します。

    通常は既存のデータベース・スキーマならびに関連する行を変更するよりもレガシー・コードを変更するほうが簡単なので、双方向の関係に変換する方法をお勧めします。

    OpenJPA での 1 対多の関係 (双方向) は、以下のようにマッピングされます。

    • 親クラスでは、one-to-many 要素で子オブジェクトのコレクションを定義し、id 要素で親オブジェクトの主キーを定義します。
    • 子クラスでは、many-to-one 要素で親オブジェクトを定義し、ネストした join-column 要素で子クラスに外部キーを定義してから、親と子の結合方法を指定します。

    マッピング 15. 1 対多の関係 (OpenJPA の XML マッピング)



    Mapping 15. One-to-many relationship (OpenJPA XML mapping)
                            
    <!-- Address (parent) class -->
    <entity class="Address">
       <table name="T_ADDRESS"/>
       <attributes>
          <id name="addressId">
             <column name="ADDR_TID"/>
          </id>
          <one-to-many name="phones" mapped-by="address">
             <cascade>
                <cascade-all/>
             </cascade>
          </one-to-many>
          ...
       </attributes>
    </entity>
    
    <!-- Phone (child) class -->
    <entity class="Phone">
       <table name="T_PHONE"/>
       <attributes>
          <many-to-one name="address">
             <cascade>
                <cascade-all/>
             </cascade>
             <join-column name="ADDR_TID"/>
          </many-to-one>
          ...
       </attributes>
    </entity>

また 1 対多の関係を定義する際によく使われる追加の機能がいくつかありますが、これらの機能についてはマイグレーションする方法を知っておく必要があるかもしれません。

  • Hibernate

    • inverse="true
      Hibernate では、双方向の関係の定義に inverse="true" 属性が使用されている場合がありますが、心配する必要はありません。OpenJPA でこの機能に相当するのは、外部キーのないテーブルを持つ、関係を所有される側です。同様に、Hibernate の inverse="false" 属性は、外部キーがあるテーブルを持つ OpenJPA での関係を所有する側に相当します。

      全体としてこれらの概念は一致していますが、誰かが双方向関係の多対 1 側に inverse="true" を設定して Hibernate のマッピングを定義してしまった場合は例外です。その場合には、マイグレーションを行う前にマッピングを変更してください。このようなマッピングは Hibernate でのベスト・プラクティスではなく、最適な SQL を生成しないからです。例えば、多対 1 が inverse="true" に設定されていると、子を作成するたびに Hibernate は 2 つの SQL 文を実行することになります。1 つは子を作成するための文、そしてもう 1 つは親の外部キーで子を更新するための文です。この場合、多対 1 側で属性を inverse="false" に変更し、1 対多側に inverse="true" を設定することで、この見落としを修正し、OpenJPA と一致させることができます。もちろん変更を行った後にはアプリケーションを再テストしてください。

    • cascade="all-delete-orphan"
      この Hibernate の機能に相当する機能は JPA 仕様にはありませんが、OpenJPA 永続プロバイダーには、Hibernate での all-delete-orphan 機能とまったく同じように作用する独自の注釈、@ElementDependent があります。この機能を使用する場合は、OpenJPA User’s Guide を参照してください。この注釈は独自の機能ですが、これを使用して all-delete-orphan 機能をマイグレーションすれば、アプリケーションのソース・コードをほとんど、あるいはまったく変更しなくても済みます。

      標準に準拠した形で all-delete-orphan 機能をマイグレーションするには、OneToMany 注釈で cascade=CascadeType.ALL 属性を指定し、親のコレクションから子を削除するだけでなく、明示的に子をデータベースから削除するようにソース・コードを変更してください。例えば以下のようなコードがあるとします。

      public class Address implements Serializable {
         ...
         public void removePhone(Phone p) {
            this.phones.remove(p); // Explicitly remove p from the set; which
                                   // Implicitly deletes p from the database
         }	
      }

      上記のコードは、以下のようなコードに置き換えることになります。

      public class Address implements Serializable {
         ...
         public void removePhone(Phone p) {
            // Explicitly remove p from the set 
            this.phones.remove(p); 
            
            // Explicitly delete p from the database
            ORMHelper.getCurrentSession().delete(p); 
         } 
      }

  • OpenJPA

    • cascade=CascadeType.ALL
      上記に記載した Hibernate の例では親から子への演算のカスケードしか指定していませんが、このマッピングを OpenJPA 永続プロバイダーでテストを行った際には、両方の方向、つまり OneToMany 注釈と ManyToOne 注釈の両方で cascade=CascadeType.ALL を定義しなければなりませんでした。通常は (ManyToOne 注釈内での) 子から親へのオペレーションのカスケードは望ましいことではありませんが、親から子への merge() オペレーションのカスケードを機能させるためには、このように定義する必要がありました。

      両方の方向でカスケードを定義する前は、親 Address オブジェクトからその従属 Phone オブジェクトへの remove() および persist() オペレーションのカスケードは機能しましたが、merge() のカスケードでは、従属オブジェクトの Phone.address フィールドにはカスケードを使用できないことを示す例外が発生しました。

      (OpenJPA v0.9.7 で明らかになったこの問題については現在対処中なので、今後のリリースでは修正されているはずです。とりあえずの手段としては、両方の方向でカスケードを有効にしてください。)

d. 多対多の関係

多対多の関係が定義するのは、マッピング・テーブルを介したオブジェクトのコレクションへの参照です。オブジェクト・モデルでは多対多の関係はそれほど一般的ではありませんが、通常は双方向となります。

他の双方向の関係と同じく所有する側と所有される側がありますが、この関係の場合、所有する側が持つのは外部キーではなくマッピング・テーブルです。関係のどちらの側を所有する側として指定するかは、自由に選択することができます。

  • オブジェクト・モデル


    マッピング 16. 多対多の関係 (POJO)
                            
    // Group (non-owner/parent) entity
    public class Group implements Serializable {
        private Long groupId;
        private Set users = new HashSet();
        ...
    }
    
    // User (owner/child) entity
    public class User implements Serializable {
        private Long userId;
        private Set groups = new HashSet();
        ...
    }

  • Hibernate での規則

    Hibernate での多対多の関係は、以下のようにマッピングされます。

    • 所有される側では、inverse="true" 属性、table 属性、key サブ要素、および many-to-many サブ要素を指定したコレクション (set、bag、または list) 要素を使用します。
    • 関係を所有する側では、table 属性、key サブ要素、および many-to-many サブ要素を指定したコレクション (set、bag、または list) 要素を使用します。


    マッピング 17. 多対多の関係 (Hibernate の XML マッピング)
                            
    <!?Group (non-owner/parent) class -->
    <class name="Group" table="T_GROUP">
        <id name="groupId" column="GROUP_TID"/>
        <set name="users" table="T_USER_GROUP" inverse="true">
            <key column="GROUP_TID"/>
            <many-to-many column="USER_TID" class="User"/>
        </set>
    </class>
    
    <!?User (owner/child) class -->
    <class name="User" table="T_USER">
        <id name="userId" column="USER_TID"/>
        <set name="groups" table="T_USER_GROUP">
            <key column="USER_TID"/>
            <many-to-many column="GROUP_TID" class="Group"/>
        </set>
    </class>

  • OpenJPA での規則

    OpenJPA での多対多の関係は、以下のようにマッピングされます。

    • 所有する側/子では、many-to-many 要素およびネストした join-table 要素を使用して、関係の作成方法を指定します。id に column 要素をネストさせて、join-table が参照する主キーの名前を指定することもできます。
    • 所有される側/親では、mapped-by 属性を指定した many-to-many 要素を使用して、結合テーブルが宣言されているクラスのプロパティーを参照します (結合テーブルには関係を作成するためのすべての情報が含まれます。id 要素を使用して主キーを作成する必要もあります)。

      マッピング 18. 多対多の関係 (OpenJPA の XML マッピング)
                                      
      <!-- User (owner/child) class -->
      <entity class="User">
         <table name="T_USER"/>
         <attributes>
            <id name="userId">
               <column name="USER_TID"/>
            </id>
            <many-to-many name="groups">
               <join-table name="T_USER_GROUP">
                  <join-column name="USER_TID"/>
                  <inverse-join-column name="GROUP_TID"/>
               </join-table>
            </many-to-many>
            ...
         </attributes>
      </entity>
      
      <!-- Group (non-owner/parent) class -->
      <entity class="Group">
         <table name="T_GROUP"/>
         <attributes>
            <id name="groupId">
               <column name="GROUP_TID"/>
            </id>
            <many-to-many name="users" mapped-by="groups"/>
            ...
         </attributes>
      </entity>

3. 遅延初期化

遅延初期化 (Lazy initialization) はどのフィールドにも適用できますが、1 対多または多対多の関係で親オブジェクトを読み取るときに、データベースにすべての子オブジェクトを返させるかどうかを制御するために使用するのが最も一般的です。

この概念を詳しく説明するため、A から B に対する 1 対多または多対多の関係を例に挙げてみましょう。この関係でフェッチを LAZY に設定すると、データベースからの A の読み取りでは、コードが A から B への関係をトラバーサルする試みを行うまではデータベースから B が読み取られることはありません。その一方、フェッチを EAGER に設定すると、データベースからの A の読み取りでは従属する B オブジェクトもデータベースから読み取られます。

LAZY または EAGER フェッチを使った関係は慎重に定義しなければなりません、特に EJB 層が分離エンティティーを Web 層に返し、Web 層がそのデータにアクセスしてビューをレンダリングするという典型的なパターンに従っている場合は要注意です。遅延ロード (Lazy loading) により、Web 層にビューのレンダリングに必要な従属オブジェクトが揃わない可能性があるためです。その一方、単純にすべての関係のロードを EAGER にすることもできません。この場合、必要以上の情報が毎回返されてしまいます。

要するに、ドメイン・モデルは必要とされる機構の要件に適したフェッチ手法を使用して調整しなければならないということです。ユース・ケースに親オブジェクトと子オブジェクトが必要な場合は EAGER ロードを使用し、ユース・ケースに子オブジェクトが必要でない場合は LAZY を使用してください。

  • オブジェクト・モデル


    マッピング 19. 遅延初期化 (POJO)
                            
    // Address (parent) entity
    
    public class Address implements Serializable {
        private Long addressId;
        private Set phones = new HashSet();
        ... 
    }        
    
    // Phone (child) entity
    
    public class Phone implements Serializable {
        private Address address;
        ...
    }

  • Hibernate での規則

    Hibernateでは、1 対多または多対多の関係を実装するコレクションにはデフォルトで遅延初期化が設定されます。遅延初期化を無効にするには (つまり、積極的初期化 (Eager initialization) を有効にするには)、以下のようにマッピングします。

    • 親では、lazy=false 属性を指定したコレクション (set、bag、または list) を使用します。
    • 子では、lazy=false 属性を指定したクラスを使用してフェッチを有効にします。


    マッピング 20. 遅延初期化 (Hibernate の XML マッピング)
                            
    <!-- Address (parent) class -->
    <class name="Address" table="T_ADDRESS">
       ...
       <id name="addressId" column="ADDR_TID"/>
    
       <set
            name="phones"                 
            table="T_PHONE"
            cascade="all-delete-orphan"
            inverse="true"
            lazy="false">
            <key column="ADDR_TID"/>
            <one-to-many class="Phone"/>
       </set>
    
    </class>
    
    <!-- Phone (child) class -->
    <class name="Phone" table="T_PHONE" lazy="false">
       ...
       <many-to-one
          name="address"
          class="Address"
          column="ADDR_TID">
       </many-to-one>
    </class>

  • OpenJPA での規則

    OpenJPA でも、1 対多および多対多の関係には遅延初期化がデフォルト設定されます。遅延初期化を無効にするには (つまり、積極的初期化を有効にするには)、以下のようにマッピングします。

    • 親エンティティーで、コレクションに fetch=FetchType.EAGER 属性を指定します。


    マッピング 21. 遅延初期化 (OpenJPA の XML マッピング)
                            
    <!-- Address (parent) class -->
    <entity class="Address">
       <table name="T_ADDRESS"/>
       <attributes>
          <id name="addressId">
             <column name="ADDR_TID"/>
          </id>
          <one-to-many name="phones" mapped-by="address" fetch="EAGER">
             <cascade>
                <cascade-all/>
             </cascade>
          </one-to-many>
          ...
       </attributes>
    </entity>
    
    <!-- Phone (child) class -->
    <entity class="Phone">
       <table name="T_PHONE"/>
       <attributes>
          <many-to-one name="address">
             <cascade>
                <cascade-all/>
             </cascade>
             <join-column name="ADDR_TID"/>
          </many-to-one>
          ...
       </attributes>
    </entity>

Hibernate と OpenJPA では、分離オブジェクトから遅延ロードされたコレクションにアクセスする際に違いがあることも注目に値する点です。プログラマーが分離オブジェクトで遅延ロードされたコレクションにアクセスしようとすると、Hibernate では例外がスローされますが、OpenJPA では例外ではなく NULL 値が返されます。

この違いの理由は、JPA 仕様では、分離オブジェクトの遅延ロードされたコレクションへのアクセスをどのように処理するかを指定していないためです。処理方法はそれぞれの JPA ベンダーが決定できるため、例外をスローする場合も、初期化しないままにしておく場合も、さらにはゼロ要素を持つコレクションを返す場合まであります。

このような理由から、レガシー Hibernate アプリケーションが例外を使って分離オブジェクトの遅延ロードされたコレクションへのアクセスを検出している場合は、NULL コレクションに対してテストを行うことで OpenJPA でも同じようにできます。ただし重要な点として、JPA 仕様では例外を発生するか NULL を返すかは規定していないため、この動作への依存は移植不可能で、変更されたり、あるいは今後のリリースでアプリケーションが壊れてしまう可能性もあることに注意してください。

もう 1 つ重要な点は、例外を受け取る (Hibernateの場合) のか受け取らない (OpenJPA の場合) のかがテスト中に検出して修正する必要のあるアプリケーション・エラーの指標だということです。つまり、問題は例外を受け取るかどうかなので、JPA 仕様で例外をスローするか否かを要件にしても問題は解決しません。アプリケーションに必要なのは、分離オブジェクト操作のアーキテクチャーに関するガイドラインです。

上記パラグラフでは、JPA 仕様で定義されるように標準の JPA 処理を定義しましたが、OpenJPA は

openjpa.DetachState 構成プロパティーを介して追加機能を提供しています。その機能とは、この DetachState 構成プロパティー用のプラグイン・プロパティーのひとつである、AccessUnloaded です。AccessUnloaded プロパティーを false に設定すると、OpenJPA は ロードされないフィールドにアクセスした場合は必ず例外をスローします。この動作は Hibernate と一貫しています。マッピング 22 は、openjpa.DetachState 構成プロパティーの例です。


マッピング 22. openjpa.DetachState 構成プロパティー
                
<persistence ...>
	<persistence-unit name="JPATEST">
		<properties>
			<property 
				name="openjpa.DetachState" 
				value="fetch-groups(DetachedStateField=true,
					DetachedStateManager=true,
					AccessUnloaded=false)" >
			</property> 
		</properties>
	</persistence-unit>
</persistence>

分離オブジェクトの操作に対するソリューションには、以下の 3 つがあります。

  1. ビューに必要なすべてのコレクションを明示的に初期化してから返します。つまり、遅延ロードがコレクションの特定の関係に適切かどうかを自分で判断するわけです。
  2. ビューのレンダリングが完了するまでエンティティー・マネージャーを開放したままにします (つまり、ビューのエンティティー・マネージャーを手動で開始、終了します)。これにより、分離オブジェクトをまとめて操作しなくても済むようになります。
  3. EJB 3.0 での 3 つ目のソリューションは 2 つ目のソリューションと似ていて、拡張永続コンテキストを使用してトランザクション間でエンティティー・マネージャーを有効にしておきます。

エンティティーを EJB コンポーネントから Web 層に渡している場合は、最初のソリューションに従ってください。このソリューションでは、Web 層にトランザクションを管理させる必要がなく、EJB (セッション・ファサード) にトランザクション・ロジックを保持することができます。また、Web 層でトランザクションを管理するのが難しい理由は、それには (おそらく ServletFilter 内で) トランザクションをコミットし、ビューのレンダリング後にエンティティー・マネージャーを終了させなければならないためです。

分離オブジェクトに関して最後に注意しておくことは、オブジェクト・モデルは必要とされる機構のユース・ケースに合わせなければならないということです。一部のユース・ケースにはコレクションの遅延ロードが必要で、残りのユース・ケースには同じコレクションの積極的ロード (Eager loading) が必要であれば、オブジェクト・モデルではコレクションの遅延ロードを使用し、必要に応じてセッション・ファサードで積極的ロードを強制してください。つまり、セッション・ファサードでそれぞれのユース・ケースに対して異なるメソッド (ただ単に分離オブジェクトを返すメソッドや、子オブジェクトをロードしてから分離オブジェクトを返すメソッドなど) を定義するということです。OpenJPA で子オブジェクトをロードする方法は 3 通りあります。最初の 2 つは JPA 標準、最後の方法は OpenJPA 拡張です。

  1. コレクションで size() を呼び出すことによって、コレクションのロードをトリガーします。
  2. JP-QL の Fetch Join 機能を使用して、Lazy フェッチ・タイプを一時的にオーバーライドします。
  3. OpenJPA FetchGroups 注釈を使用して子オブジェクトをロードします。

4. オブジェクト ID

データ・モデル内のすべてのテーブルには、それぞれのテーブルの主キーとして、OID と呼ばれるオブジェクト ID 列が組み込まれます。OID のベスト・プラクティスは、整数列を使用し、列の値をシステムによって割り当てさせることです。こうすれば、OID は java.lang.Long を使用してオブジェクト・モデルの主キーにマッピングします。その上、テーブル同士のすべての関係 (外部キー) は関連するテーブルの OID 列に基づくことになります。

ID は、新しい行を挿入した時点で Hibernate によって割り当てられます。その手段として通常使用するのは Hibernate Sequence ジェネレーター・クラスで、このクラスはテーブルごとの単一の行にテーブルの名前と現行 OID 番号が含まれる基礎データベースのシーケンス・サポートを使用します。この方法では、OID は各テーブルに固有のものとなります (OID はテーブル全体で固有でない場合もあります)。

OID を使用すると、必要とされる機構にとって意味を持たないすべてのデータにキーが提供されます。システムによって生成されるこれらのキーは、その機構では意味を持たないため、機構にとって意味のある自然キーの対語として合成キーと呼ばれます。合成キーを使用することで、データベース内のデータの中で機構にとって意味のあるあらゆるデータを、制約に違反する心配なく変更することができます。

既存の Hibernate モデルでは、大部分のテーブルが、多数の列を組み込むことができ、機構にとって意味のある ID を持っているということが、通常わかるはずです。また、データ・モデル内のこのような主キー候補に、固有の制約が定義されている場合もあります。このデータベース制約は一意性を確実にするとともに、機構にとっての意味を基準にエンティティーを検索するための索引にもなります。

考えられる特殊な状況として、機構にとって意味を持たないデータ行を示すために、一部のテーブルにデータを事前設定しなければならないことがあります (コード・テーブルなどの場合)。そのためには、アプリケーションが起動される前に該当する行がデータベースに挿入済みであること、そしてこれらの行の OID が該当するテーブルの シーケンス・テーブルの開始値よりも小さい値であることが必要です。このような場合、シーケンス・テーブルの初期値が 1 より大きい値になっていることがあります。

  • オブジェクト・モデル


    マッピング 23. オブジェクト ID (POJO)
                            
    / Address entity
    
    public class Address implements Serializable {
        private Long addressId;
        ...
    }

  • Hibernate での規則

    Hibernate でのオブジェクト ID は、以下のようにマッピングされます。

    • generator サブ要素をネストした id を使用します。


    マッピング 24. オブジェクト ID (Hibernate の XML マッピング)
                            
    <class name="Address">
        ...
        <id name="addressId" column="ADDR_TID">
            <generator class="sequence">
                <param name="sequence">T_ADDRESS_SEQ</param>
            </generator>   
        </id>
       ...
    </class>

  • OpenJPA での規則

    OpenJPA では、上記と同じオブジェクト ID は以下のようにマッピングされます。

    • Id、SequenceGenerator、および GeneratedValue 注釈を使用します。


    マッピング 25. オブジェクト ID (OpenJPA の XML マッピング)
                            
    /<!-- Address entity -->
    <entity class="Address">
      <sequence-generator name="AddressSeq" sequence-name="T_ADDRESS_SEQ"/>
      <attributes>
        <id name="addressId">
          <column name="ADDR_TID"/>
          <generated-value strategy="SEQUENCE" generator="AddressSeq"/>
        </id>
        ...
      </attributes>

Hibernate と OpenJPA では、OID の既存のシーケンス・テーブルに対する DDL は以下のようになります。

create sequence T_ADDRESS_SEQ;

オブジェクト ID に関する最後の注意として、データ・モデル内のすべての外部キーは通常、関連するテーブルの OID 主キーに基づいています。主キーを参照し、ON DELETE RESTRICT セマンティクスが定義されているデータ・モデルではすべての外部キーに制約が設定されるため、該当する親の子オブジェクトに対応する行がまだデータベース内に存在している場合はデータベースから行を削除できないようになっています (例えば、親オブジェクトを削除することはできません)。

5. オプティミスティック・ロック

Hibernate では並行性を制御するためにオプティミスティック・ロックとペシミスティック・ロックという 2 つのモードが用意されています。短期間のトランザクションとパフォーマンス改善に対応するため、ほとんどのレガシー Hibernate アプリケーションではオプティミスティック・ロックを使用します。短期間のトランザクションではユーザーが画面上でデータを編集している間、ロックを解放しますが、オプティミスティック・ロックはオブジェクトのタイムスタンプあるいはバージョンをチェックして、ユーザーがデータを編集している間、他には誰もオブジェクトを変更しなかったことを確認します。変更されている場合はオプティミスティック・ロック例外がスローされ、ユーザーに例外が発生したことが通知されます。これにより、ユーザーは例外の原因となったデータを更新してトランザクションを再試行することができるので、次回はうまく動作するはずです。ユーザーへのオプティミスティック・ロック例外の通知は、通常、アプリケーションの標準的な例外処理によって行われます。

  • オブジェクト・モデル

    並行性が問題になる可能性がある場合、すべてのエンティティーにはバージョン・プロパティーを含めます。永続プロバイダーはこのプロパティーを使用して、同じレコードに対する同時変更を検出します。レガシー・アプリケーションではバージョン・プロパティーを単純に不変のプロパティーとして扱うため、エンティティーがデータベースから読み取られてデータベースに再び書き込まれるまで、バージョンとエンティティーを同調させなければなりません。



    マッピング 26. オブジェクト ID (POJO)
                            
    // Address entity
    
    public class Address implements Serializable {
       private Long version;
        ...
    }

  • Hibernate での規則

    Hibernate でのオプティミスティック・ロックは、以下のようにマッピングされます。

    • version 要素を使用してバージョン・プロパティーを定義します。


    マッピング 27. オプティミスティック・ロック (Hibernate の XML マッピング)
                            
    <!-- Address class -->
    <class name="Address" table="T_ADDRESS">
        ...
        <version name="version" column="VERSION"/>
       ...
    </class>

  • OpenJPA での規則

    OpenJPA でのオプティミスティック・ロックは、以下のようにマッピングされます。

    • version 要素を使用してバージョン・プロパティーを定義します。


    Mマッピング 28. オプティミスティック・ロック (OpenJPA の XML マッピング)
                            
    <!-- Address class -->
    <entity class="Address">
       <table name="T_PHONE"/>
       <attributes>
          ...
          <version name="version">
             <column name="VERSION"/>
          </version>
          ...
       </attributes>
    </class>

    Hibernate ではオプティミスティック・ロックとペシミスティック・ロックの両方をサポートしますが、JPA 仕様ではオプティミスティック・ロックの概念しか定義していません。ただし、OpenJPA ではオプティミスティック・ロックをサポートし、ペシミスティック・ロック用の拡張を提供しています。そのため、ペシミスティック・ロックを使用するレガシー Hibernate アプリケーションをマイグレーションする場合は、persistence.xml ファイルに以下の OpenJPA 構成プロパティーを設定することを検討してください。



    マッピング 29. ペシミスティック・ロック (OpenJPA 構成プロパティー)
                            
    <property 
       name="openjpa.LockManager" 
       value="pessimistic">
    </property>
    <property 
       name="openjpa.ReadLockLevel" 
       value="read">
    </property>
    <property 
       name="openjpa.WriteLockLevel" 
       value="write">
    </property>
    <property 
       name="openjpa.jdbc.TransactionIsolation" 
       value="repeatable-read">
    </property>

    上記のプロパティーを使用すると、すべての読み取りで共有 (読み取り) ロックが取得され、トランザクションの終了時まで保持されます。同じレコードに対して同時に更新するユーザーが複数いるために並行性の問題 (デッドロック) がある場合は、ReadLockLevel を write に指定し、データを取得する際に FOR UPDATE を生成して更新の逐次化を強制する必要が出てくることも考えられます。

    persistence.xml ファイルにある構成パラメーターを使用して上記のペシミスティック・ロック・レベルを指定すると、その設定はすべてのトランザクションに適用されます。代替手段としては、org.apache.openjpa.persistence.FetchPlan クラスを使用して、これらの構成プロパティーを個別のトランザクションごとにプログラムによって設定することも可能です。この場合のコード・フラグメントを以下に記載します。



    マッピング 30. ペシミスティック・ロック (プログラムによる OpenJPA 構成プロパティーの設定)
                            
    import org.apache.openjpa.persistence.*;
    ...
    EntityManager em = ...;
    em.getTransaction ().begin ();
    ...
    // load an object with a pessimistic read lock mode
    fetch = ( OpenJPAPersistence.cast( em ) ).getFetchPlan();
    fetch.setReadLockMode( LockModeType.WRITE );
    Address address = em.find( Address.class, addressId );
    ...
    em.getTransaction ().commit ();




上に戻る


Hibernate の構成パラメーターのマイグレーション

Hibernate で SessionFactory の構成方法として最も一般的なのは、hibernate.cfg.xml ファイルに <property> 要素を含め、そのファイルをクラスパスのルート・フォルダーに配置することです。使用される回数は少ないとは言え、これに相当する手法としては、クラスパスの hibernate.properties ファイルにプロパティーを含めるという方法もあります。

OpenJPA で EntityManagerFactory を構成する場合は、persistence.xml ファイルに名前付き <persistence-unit> 要素を組み込み、このファイルをクラスパスの META-INF フォルダーに配置します。<persistence-unit> は永続プロバイダーやマッピング・ファイル、そしてデータベース接続やロギングなどのその他のプロパティーを定義する要素です。永続プロバイダーは JPA 仕様を実装するベンダーを識別するため、<persistence-unit> 内の名前付きプロパティーはその永続プロバイダーに固有となります。つまり、この場合は OpenJPA に固有のプロパティーということです。

Hibernate アプリケーションを OpenJPA にマイグレーションする際には、少なくとも 3 つの共通する構成シナリオがあります。

  1. データベース接続 -- SessionFactory にデータベースとの接続方法を指示する構成プロパティー
  2. マッピング位置 -- データベース内の行へのオブジェクトのマッピングを管理するプロパティー
  3. ロギング・カテゴリー -- ロギング/トレースのレベルの設定など、問題を診断できるようにするためのプロパティー

目にする可能性のある構成プロパティーは多すぎて記載しきれないので、その他の構成プロパティーのマッピングについては「参考文献」で確認してください。該当する資料は Hibernate API Documentation (org.hibernate.cfg.Environment クラスとすべての Hibernate 構成プロパティーの場合)、そして OpenJPA User's Guide です。

1. データベース接続

データベース接続を構成するには、ローカル JDBC 接続を使用するか (ここで説明)、あるいは J2EE データ・ソースを使用します (「参考文献」を参照)。

  • Hibernate での規則

    Hibernate での JDBC 接続の構成は、以下のようにマッピングされます。

    • dialect 構成パラメーターを使用します。
    • connection.driver_class 構成パラメーターを使用します。
    • connection.url 構成パラメーターを使用します。
    • connection.username 構成パラメーターを使用します。
    • connection.password 構成パラメーターを使用します。


    構成 1. Hibernate のデータベース接続
                            
    ...
    <hibernate-configuration>    
       <session-factory>                   
    	<property 
             name="dialect">
             org.hibernate.dialect.DB2Dialect
          </property>
    	<property
             name="connection.driver_class">
             com.ibm.db2.jcc.DB2Driver
          </property>
    	<property
             name="connection.url">
             jdbc:db2://localhost:50000/HIBTEST
          </property>
    	<property 
             name="connection.username">
             db2admin
          </property>
    	<property 
             name="connection.password">
             db2admin
          </property>
       </session-factory>
    </hibernate-configuration>

  • OpenJPA での規則

    OpenJPA での相当する JDBC 接続の構成は、以下のようにマッピングされます。

    • openjpa.jdbc.DBDictionary 構成パラメーターを使用します (OpenJPA では通常、URL および DriverName プロパティーによって正しい辞書を判断できるので、この構成パラメーターはオプションと考えられます)。
    • openjpa.ConnectionDriverName 構成パラメーターを使用します。
    • openjpa.ConnectionURL 構成パラメーターを使用します。
    • openjpa.ConnectionUserName 構成パラメーターを使用します。
    • openjpa.ConnectionPassword 構成パラメーターを使用します。


    構成 2. OpenJPA のデータベース接続
                            
    <persistence ...>
       <persistence-unit name="JPATEST">
          <properties>
    	   <property 
                name="openjpa.jdbc.DBDictionary" 
                value="db2(DriverVendor=db2)" >
             </property> 
             <property 
                name="openjpa.ConnectionDriverName"
                value="com.ibm.db2.jcc.DB2Driver">
             </property>
             <property 
                name="openjpa.ConnectionURL"   
                value="jdbc:db2://localhost:50000/JPATEST">
             </property>
             <property 
                name="openjpa.ConnectionUserName" 
                value="db2admin">
             </property>
    	   <property 
                name="openjpa.ConnectionPassword" 
                value="db2admin">
             </property> 
          </properties>
       </persistence-unit>
    </persistence>

    (上記の例には必要ありませんが、openjpa.ConnectionProperties という非常に便利なパラメーターもあります。このパラメーターを使用すると、connect() を呼び出すときに追加プロパティーを渡すことができます。)

2. マッピング位置

Hibernate でXML ベースの構成手法を使用すると、構成パラメーターだけでなくマッピング・ファイルの位置も指定することができます。XML ベースの構成手法では、プログラムによってマッピング・ファイルの位置を指定しなくても SessionFactory を構成することができるので、この手法を使用するのが一般的です。

  • Hibernate での規則

    Hibernate でのマッピング・ファイル位置の構成は、以下のようにマッピングされます。

    • resource 属性を指定した <mapping> を使用します。


    構成 3. Hibernate のマッピング位置
                            
    ...
    <hibernate-configuration>    
       <session-factory>                   
    	...
    	<!-- Mapping files -->
    	<mapping resource="domainmodel.hbm.xml"/>
       </session-factory>
    </hibernate-configuration>

  • OpenJPA での規則

    OpenJPA でのマッピング・ファイル位置の構成は、以下のようにマッピングされます。

    • <mapping-file> 要素を使用します。


    構成 4. OpenJPA のマッピング位置
                            
    <persistence ...>
    <persistence-unit name="TEST">
       ...      
       <!-- Mapping files -->
       <mapping-file>META-INF/orm.xml</mapping-file>
    </persistence-unit>

3. ロギング・カテゴリー

Hibernate はマッピング・ファイルに指定されたメタデータに基づいて SQL コマンドを生成するので、Hibernate アプリケーションではトラブルシューティングが困難です。そのため、たいていの場合はロギング・カテゴリーを構成して、Hibernate がデータベースに対して実行した正確な SQL を調べられるようにします。

  • Hibernate での規則

    Hibernate でのロギング・カテゴリーの構成は、以下のようにマッピングされます。

    • show_sql 構成パラメーターを使用して SQL コマンドを出力します。


    構成 5. Hibernate のロギング・カテゴリー
                            
    ...
    <hibernate-configuration>    
       <session-factory>                   
    	...
    	<property 
             name="show_sql">
             true
          </property>
       </session-factory>
    </hibernate-configuration>

    SQL を調べるだけでは問題を診断できない場合、Hibernate では他のロギング・カテゴリーも使用できますが、これらのカテゴリーを構成する場所はクラスパスの log4j.propertiesファイルです (このファイルの構成はここには記載しません。その他の Hibernate ロギング・カテゴリーの構成方法については、「Hibernate の資料を参照してください)。

  • OpenJPA での規則

    OpenJPA でのロギングの構成は、以下のようにマッピングされます。

    • openjpa.Log 構成プロパティーを使用して SQL を出力します。
    • openjpa.ConnectionFactoryProperties を使用して読みやすい SQL を出力します。


    構成 6. OpenJPA のロギング・カテゴリー
                            
    <persistence ...>
    <persistence-unit name="TEST">
       ...      
       <properties> 
    	<property 
             name="openjpa.Log" 
             value="SQL=TRACE">
          </property> 
          <property
             name="openjpa.ConnectionFactoryProperties"
             value="PrettyPrint=true, PrettyPrintLineLength=72">
          </property>
      </properties>
    </persistence-unit>

    openjpa.Log 構成プロパティーを使用して、他のロギング情報を出力するように OpenJPA を構成することもできます (OpenJPA User's Guide を参照)。




上に戻る


まとめ

この記事では、EJB 2.1 を使用した独自仕様の Hibernate 3 アプリケーションを、EJB 3.0 準拠の OpenJPA 0.9.7 永続プロバイダーを使用した業界標準の JPA アプリケーションにマイグレーションするという事例研究について説明しました。このマイグレーションを説明するために使用したのは一連の一般的な方法で、それぞれの方法では Hibernate と OpenJPA の実装を併記して比較しました。この比較は、既存の Hibernate/EJB 2.1 アプリケーションを OpenJPA/EJB 3.0 にマイグレーションしている読者だけでなく、次のプロジェクトで OpenJPA/EJB 3.0 を使用する可能性のある読者にとって役立つはずです。

レガシー Hibernate アプリケーションを構成する主要な 3 つのビルディング・ブロック (アプリケーション・ソース・コード、オブジェクト・リレーショナル・マッピング、構成パラメーター) のマイグレーション方法を説明したこの記事によって引き出された結論は以下のとおりです。

  • Hibernate アプリケーションのソース・コードが適切に構造化されていて、Hibernate の呼び出しをカプセル化していれば、一般的なシナリオではソース・コードを簡単にマイグレーションすることができます。Hibernate の呼び出しをカプセル化していないプログラムの場合、マイグレーションの難易度は高くなりますが、それでも必要とされる機構 (セッション、トランザクション、エンティティー管理) のロジックの変更よりは構文の変更が多くを占めます。

  • オブジェクト・リレーショナル・マッピングはすでに存在するため、meet-in-the-middle のマイグレーション方式を使って既存のオブジェクト・モデルとデータ・モデルを維持しなければなりません。ボトムアップ方式でデータ・モデルからオブジェクト・モデルを生成することはできませんし、トップダウン方式でオブジェクト・モデルからデータ・モデルを生成することもできません。マイグレーションでは両方のモデルを維持する必要がるのです。この記事では中間突き合せ方式のマッピングを手作業で実装しましたが、Dali JPA Tools や IBM Design Pattern Toolkit (「参考文献」を参照) を使用すれば、Hibernate XML から OpenJPA XML へのマイグレーションの大部分を自動化できる可能性があります。

  • 共通構成パラメーターは簡単に Hibernate から OpenJPA にマイグレーションできますが、レガシー Hibernate アプリケーションの調整に使われているその他の多くのパラメーターは OpenJPA に必要な場合もそうでない場合もあります。したがって、並べてマイグレーションを行っても調整することはできません。共通構成パラメーターをマイグレーションしてから負荷テストを行い、そのテスト結果で、どの OpenJPA 調整パラメーターを導入するべきかを調べてください。

この記事では Hibernate クエリーの OpenJPA へのマイグレーションについては説明しませんでしたが、その理由は、Hibernate アプリケーションには必要とされる機構の要件と一致する明確に定義されたドメイン・モデルがあるのが一般的だからです。そのようなドメイン・モデルがなく、臨機応変な多数のクエリーが必要なアプリケーションには、Hibernate や OpenJPA といったオブジェクト・リレーショナル・マッピング・ツールよりも、iBatis のようなソリューションがふさわしいはずです。そうは言うものの、Hibernate クエリーの OpenJPA へのマイグレーションが十分興味の対象となった場合には、今後の記事でこの話題について取り上げたいと思います。




上に戻る


付録: OpenJPA の Java 5 注釈


注釈 A. 単一のテーブルによる継承
                
//Participant (base) class
@Entity
@Table(name="T_PRTCPNT")
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PRTCPNT_CLASS")
public class Participant implements java.io.Serializable {
   @Column(name="PRTCPNT_TID")
   @Id private Long participantId; 
   ...
}

// SalesRep subclass
@Entity
@DiscriminatorValue(value="SALES_REP")
public class SalesRep extends Participant { 
   ...
}

// Administrator subclass
@Entity
@DiscriminatorValue(value="ADMIN")
public class Administrator extends Participant { 
   ...
}


注釈 B. 結合による継承
                
// Participant (base) class
@Entity
@Table(name="T_PRTCPNT")
@Inheritance(strategy=InheritanceType.JOINED)
public class Participant implements Serializable { 
    @Column(name="PRTCPNT_TID")
    @Id private Long participantId;
    ... 
}

// SalesRep subclass

@Entity
@Table(name="T_SALESREP")
@PrimaryKeyJoinColumn(name="PRTCPNT_TID")
public SalesRep extends Participant { 
    ... 
}

// Administrator subclass

@Entity
@Table(name="T_ADMIN")
@PrimaryKeyJoinColumn(name="PRTCPNT_TID")
public Administrator extends Participant { 
    ... 
}


注釈 C. 埋め込み可能オブジェクトを使用した 1 対 1 の関係
                
// Employee (parent) class
@Entity 
@Table(name="T_EMPLOYEE")
public class Employee implements Serializable {
    @Embedded private EmployeeRecord employeeRec;
    ...
}

// EmployeeRecord (child) class
@Embeddable
public class EmployeeRecord implements Serializable {
    ...
}


注釈 D. 多対 1 の関係
                
// Address (parent) class

@Entity 
@Table(name="T_ADDRESS")
public class Address implements Serializable {
    ...
    @Column(name="ADDR_TID")
    @Id private Long addressId;
}

// Phone (child) class
@Entity 
@Table(name="T_PHONE")
public class Phone implements Serializable {
    ...
    @ManyToOne
    @JoinColumn(name="ADDR_TID")
    private Address address;
    ...
}


注釈 E. 1 対多の関係
                
// Address (parent) entity

@Entity 
@Table(name="T_ADDRESS")
public class Address implements Serializable {
    ...
    @OneToMany(mappedBy="address", cascade=CascadeType.ALL)
    private Set<Phone> phones = new HashSet<Phone>();
    ... 
    @Column(name="ADDR_TID")
    @Id private Long addressId;
}        

// Phone (child) entity

@Entity 
@Table(name="T_PHONE")
public class Phone implements Serializable {
    ...
    @ManyToOne(cascade=CascadeType.ALL) 
    @JoinColumn(name="ADDR_TID")
    private Address address;
    ...
}


注釈 F. 多対多の関係
                
// User (owner/child) entity
@Entity 
@Table(name="T_USER")
public class User implements Serializable {
    @Column(name="USER_TID")
    @Id private Long userId;

    @ManyToMany
    @JoinTable(
        name="T_USER_GROUP",
        joinColumns=@JoinColumn(name="USER_TID"),
        inverseJoinColumns=@JoinColumn(name="GROUP_TID"))
    private Set<Group> groups = new HashSet<Group>();
    ...
}

// Group (non-owner/parent) entity
@Entity 
@Table(name="T_GROUP")
public class Group implements Serializable {
    @Column(name="GROUP_TID")
    @Id private Long groupId;

    @ManyToMany(mapppedBy="groups")
    private Set<User> users = new HashSet<User>();
    ...
}


注釈 G. 遅延初期化
                
// Address (parent) entity

@Entity 
@Table(name="T_ADDRESS")
public class Address implements Serializable {
    ...
    @OneToMany(mappedBy="address", fetch=FetchType.EAGER)
    private Set<Phone> phones = new HashSet<Phone>();;
    ... 
    @Column(name="ADDR_TID")
    @Id private Long addressId;
}        

// Phone (child) entity

@Entity 
@Table(name="T_PHONE")
public class Phone implements Serializable {
    ...
    @ManyToOne
    @JoinColumn(name="ADDR_TID")
    private Address address;
    ...
}


注釈 H. オブジェクト ID
                
// Address entity

@Entity 
@Table(name="T_ADDRESS")
public class Address implements Serializable {
    ... 
    @SequenceGenerator(name="AddressSeq", sequenceName="T_ADDRESS_SEQ")
    @GeneratedValue(
        strategy=GenerationType.SEQUENCE,
        generator="AddressSeq")
    @Column(name="ADDR_TID")
    @Id private Long addressId;
    ...
}


注釈 I. オプティミスティック・ロック
                
@Entity
@Table(name="T_ADDRESS")
public class Address {
   ...
   @Column(name="VERSION")
   @Version private long version;
   ...
}




上に戻る


謝辞

この記事の作成に協力してくれた Roland Barcia 氏、Reddy Sripathi 氏、David Wisnesk 氏、そして David Zhang 氏に感謝します。



参考文献

学ぶために

議論するために


著者について

Donald Vines は、北米での WebSphere マイグレーション・プラクティスを担当する IBM のエグゼクティブ IT アーキテクトです。現在は主要な IBM 顧客と協力し、ソリューション・アーキテクチャーの作成、そして SOA およびエンタープライズ近代化プロジェクトにおける設計者とソフトウェア開発者の指導に当たっています。以前は世界最大の業界標準化団体、OMG (Object Management Group) の技術代表者を務め、現在インターネット全体で使用されている IIOP (Internet Inter-ORB Protocol) の考案にも加わりました。彼は Sun Certified Enterprise Architect、Sun Certified Java Programmer、そして OMG Certified UML2 Professional でもあります。


Kevin Sutter は、WebSphere Application Server 開発グループのシニア・ソフトウェア・エンジニアで、現在は WebSphere 対応の Java Persistence API 実装を指揮しています。彼は Apache OpenJPA プロジェクトの PMC コミッター兼メンバーでもあります。過去の職務として、JCA (J2EE Connector Architecture) および ObjectGrid (キャッシング・フレームワーク) を対象とした WebSphere ソリューションを指揮および設計した実績もあります。




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



 


 


不充分・不完全である大変素晴らしい
 


この記事を共有する

del.icio.us del.icio.us newsing newsing FC2ブックマーク FC2ブックマーク
Choix! Choix! ニフティクリップ ニフティクリップ Yahoo!ブックマーク Yahoo!ブックマーク
MM/memo MM/memo CZブックマーク CZブックマーク livedoorクリップ livedoorクリップ
はてなブックマーク はてなブックマーク Buzzurl(バザール) Buzzurl(バザール)




上に戻る


    日本IBMについて プライバシー お問い合わせ