第5回 Hibernateでインテグレーション層のDAOデザインを考える



西ヶ谷岳(サン・マイクロシステムズ)
2006/1/6

  Page.1 Page.2

 前回までの連載で、Java EE 5環境への移行に適したプレゼンテーション層、およびビジネス層のデザインの方法を紹介してきました。最終回は、インテグレーション層におけるデータベース・アクセスのデザインについて考えます。第1回で述べたように、Java EE 5に含まれるEJB 3.0では、ドメイン・モデルとして設計したPOJOなオブジェクトをそのままデータベース・アクセスに利用できるようになります。そして、その多くの機能は、現在パーシステント・レイヤのフレームワークとして最もよく使われているHibernateから引き継がれています。

 今回は、EJB 3.0で実現される機能を考えながら、J2EE 1.4環境のインテグレーション層をどうデザインすべきかを考えます。

   EJB 3.0のEntityManager

 まず、EJB 3.0のパーシスタンスAPIのドラフトから、データベース・アクセスのサンプルを見てみましょう。

リスト1 EJB 3.0におけるEntityManagerの使用例
@Stateless public class OrderEntry {
    @PersistenceContext EntityManager em;
    public void enterOrder(int custID, Order newOrder) {
        Customer cust = (Customer)em.find("Customer", custID);
        cust.getOrders().add(newOrder);
        newOrder.setCustomer(cust);
    }
}

 OrderEntryクラスは、アノーテーション@Statelessがあることからステートレスなセッション・ビーンとして動作することが示されています。また、@PersistentContextによって、EJB 3.0から導入されたパーシスタンスAPIであるEntityManagerをOrderEntryセッション・ビーンにインジェクトしています。ビジネス・メソッドentiryOrder()では、顧客IDを使ってCustomerオブジェクトを検索し、引数のOrderオブジェクトと関係付けを行っていますが、EntityManagerを使用することで非常に簡潔に表されていることが分かります。

 しかし、ここでいくつかの疑問がわいてきます。

  • データベースに対するSQL文はいつ発行されているのか?
  • EntityManagerはいつ生成されて、いつ解放されるのか?
  • EntityManagerに関連するDataSourceはいつ解放されるのか?

 HibernateからEJB 3.0への移行を考慮したインテグレーション層をデザインするためには、まずEJB 3.0のEntityManagerのライフサイクルについて理解しておくことが必要です。

   EntityManagerのライフサイクル

 EntityManagerのライフサイクルを理解するために、もう少し、リスト1のコードの振る舞いを考えてみましょう。cust.getOrders().add(newOrder)や、newOrder.setCustomer()は、POJOのオブジェクトのプロパティを変更しているだけで、CustomerやOrderにマッピングされているデータベースのテーブルには何も影響を与えていないように見えます。実際のところ、エンティティ・オブジェクトのプロパティを変更しただけでは、データベースに対してSQL文は発行されていません。EntityManagerを使用した場合、実際のSQL文が発行されるのは、EntityManagerのclose()またはflush()メソッドが発行されたとき、またはトランザクションがコミットされたときです。トランザクション境界になって初めてSQLが発行されるところはEJB 2.xのエンティティ・ビーンと同じですね。

 次に、EntityManagerのライフサイクルについて考えます。リスト1のアノーテーション@PersistenceContextには、EntityManagerのライフサイクルに関するtype属性が省略されていました。type属性を省略せずにリスト1の@PersistenceContextを記述すると以下のようになります。

    @PersistenceContext(type=PersistenceContextType.TRANSACTION)
    EntityManager em;

 type=TRANSACTIONが指定された場合、トランザクションの開始時にEntityManagerが生成され、トランザクション終了時にclose()されることを意味しています。ここでは、このライフサイクルをトランザクション単位(persistence context per transaction)、または、リクエスト単位(persistence context per request)ライフサイクルと呼びます(図1)。このライフサイクルは、最も安全で問題を起こしにくいため、@PersistenceContextのtype属性はTRANSACTIONがデフォルトになっています。

図1 EntityManagerのライフサイクル(リクエスト単位)

 type=EXTENDEDが指定された場合、EntityManagerを複数のトランザクションにまたがって生存させることができるようになります。特定のビジネス処理は複数のリクエスト/レスポンスによって成り立つ場合があります。このビジネス処理のまとまりをビジネス・トランザクションと呼びます(図2)。

図2 EntityManagerのライフサイクル(ビジネス・トランザクション単位)

 ビジネス・トランザクション内では、同じドメイン・モデルに対する操作を行うことが多いため、ドメイン・モデルのキャッシュを保持しているEntityManagerをビジネス・トランザクション内で共有した方が、余計なデータベース・アクセスが削減され、効率が良くなる可能性があります。その半面、アプリケーションを複雑にし、EntityManagerの解放忘れの問題を引き起こす可能性もあります。そのため、通常、type=EXTENDEDはステートフル・セッション・ビーンとともに使用し、セッション・ビーンの生成から解放のライフサイクルは、EntityManagerのそれと同期するように使われます。すなわち、ビジネス・トランザクションをステートフル・セッション・ビーンの生存期間にマッピングするようにするわけです。

 J2EE 1.4のコンテナ上で、EntityManagerの代わりにHibernate Sessionを使用する場合、全く同じようにSessionのライフサイクルの問題が存在します。できれば、リスト1のEJB 3.0の場合と同じように、アプリケーションの開発者があまりSessionのライフサイクルを意識しなくてもよいような仕組みを用意したいと考えます。

   SpringのAOPを用いて
 Hibernate Sessionのライフサイクルを管理する

 ここでは、J2EE 1.4の環境で、リクエスト単位のHibernate Sessionオブジェクトの暗黙のライフサイクルを実現します。図3のようにビジネス・メソッド内で一度生成されたSessionは再利用されるようにします。また、ビジネス処理の最後に生成されたすべてのHibernate Sessionを解放するには、SpringフレームワークのAOP機能を用いて、ビジネス・メソッドにインターセプターを織り込むことで実現できます。これにより、ビジネス・メソッド内にHibernate Sessionに対する操作を記述する必要がないため、Sessionの解放忘れに基づく問題を引き起こす可能性を限りなく小さくすることができます。

図3 Interceptorによるリクエスト単位ライフサイクルの実現

 Springフレームワークには、リクエスト単位のHibernate Sessionのライフサイクルを実現するため、Sessionの取得用にSessionFactoryUtilsクラス、Sessionの解放用にHibernateInterceptorクラスの実装が含まれています。しかし、残念ながらこれらの実装は、ビジネス・メソッドが単一のSessionを扱う場合にしか対応していません。アプリケーションが、複数のHibernate Session(すなわち、複数のデータソース)を扱う場合には、これらのユーティリティクラスを作成する必要があります。

 まず、同一リクエスト内でSessionが再利用されるようにするため、SessionFactoryクラスのopenSession()メソッドを隠ぺいした、以下のようなHibernateUtilクラスを用意します。


リスト2 HivernateUtilクラス
public class HibernateUtil {
    
    static ThreadLocal holder = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new HashMap();
        }
    };
    
    public static Session getSession(SessionFactory factory) {
        // ThreadLocalから取得済みのSessionを取り出す
        Map sessionMap = (Map)holder.get();
        Session session = (Session)sessionMap.get(factory);
        if (session == null) {
            // Sessionを生成し、Mapに保存する
            session = factory.openSession();
            sessionMap.put(factory, session);
        }
        
        if (!session.isConnected()) {
            // JDBCコネクションをバインドする
            session.reconnect();
        }
        return session;
    }
        :

 HibernateUtilクラスでは、同一リクエスト内で一度取得されたSessionオブジェクトが再利用されるように、ThreadLocalを用いて取得済みのSessionオブジェクトを管理しています。また、複数のSessionに対応するため、ThreadLocalはMapとして管理していることに注意してください。

 ビジネス・ロジックの最後には、使用されたSessionをすべてclose()し、解放する必要があるため、HibernateUtilクラスにこのためのメソッドcloseAllSessions()を設けます。

  public static void closeAllSessions() {
        // ThreadLocalにあるすべてのSessionをクローズする
        Map sessionMap = (Map)holder.get();
        for (Iterator it = sessionMap.values().iterator();
        it.hasNext();) {
            Session session = (Session)it.next();
            if (session != null && session.isOpen()) {
                session.close();
            }
        }
        // Mapを空にして、すべてのSessionを解放する
        sessionMap.clear();
    }

 このcloseAllSessions()は、HibernateInterceptorクラスによって、自動的にすべてのビジネス・メソッドで実行されるようにします。HibernateInterceptorの実装は、ポイントカットのafter adviceとして、closeAllSessions()を呼ぶだけのシンプルなものです。

リスト3 HibernateInterceptorクラス
  class HibernateInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation inv) throws Throwable {
        try {
            return inv.proceed();
        } finally {
            HibernateUtil.closeAllSessions();
        }
    }
}


 

 Hibernateを用いたデータアクセス

 上述の2つのユーティリティクラスを用意することにより、Hibernateによるデータアクセスのコードは、EntityManagerを使用した場合と同程度にシンプルなものになります。例えば、以下は、指定された顧客IDに対応する顧客情報をテーブルから削除するCustomerDAOのdelete()メソッドの実装例です。

リスト4 CustomerDAOImplクラスの実装例
public class CustomerDAOImpl implements CustomerDAO {

    private SessionFactory sessionFactory;
    
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
    
    public void delete(int id) {
        Session session = HibernateUtil.getSession(sessionFactory);
        session.createQuery("delete from CustomerBO where id = :id")
            .setInteger("id", id)
            .executeUpdate();
    }
        :
}

 delete()メソッドの1行目はHibernateUtilクラスを用いて、SessionFactoryからSessionオブジェクトを取得しています。このSessionFactoryは、SpringフレームワークのDI機能を用いて外部から設定されることを想定しています。

 delete()メソッドの2行目以降が実際に削除処理を実行している部分です。Hibernateはバージョン3.0から削除と更新についてもHQL(Hibernate専用の問い合わせ言語)が利用できるようになりました。また、HQLはEJB 3.0のEJB-QLと非常によく似た表現となっており、:idのような名前付きパラメータも同じ書式で使用することが可能です。リスト4のHQLはそのまま、EJB-QLとしても使用することができますので、EntityManagerを使用したdelete()メソッドへの書き換えは簡単です。

リスト5 CustomerDAOImplクラスの実装例(EntityManager使用)

public class CustomerDAOImpl implements CustomerDAO {

   @PersistenceUnit EntityManagerFactory emf;

   public void delete(int id) {
      EntityManager em = emf.createEntityManager();
        emf.createQuery("delete from CustomerBO where id = :id")
        .setParameter("id", new Integer(id))
        .executeUpdate();
    }
         :
}

 同様に、顧客名と会社名をキーにして、条件に一致するCustomerBOのリストを取得するfind()メソッドを定義します。Hibernateでは、テーブル検索の方法として、以下の3つの方法があります。

  • HQL表現による検索
  • Criteriaユーティリティによる検索
  • ネイティブSQLによる検索

 Criteriaは大変便利で簡潔に問い合わせを行うことができますが、EJB 3.0にはCriteriaに相当するインターフェイスがありません。また、ネイティブなSQLはEJB 3.0でも使用することができますが、データベースベンダ固有の文法に依存したコードになりやすいため、なるべくHQLを使用して問い合わせ文を定義するようにします。

 find()メソッドは、検索条件によって問い合わせ文の構造が変わってきます。

  • 条件が指定されなかった場合、WHERE句はなし
  • 顧客名のみ指定された場合、WHERE句に顧客名の部分一致条件を付ける
  • 会社名のみ指定された場合、WHERE句に会社名の部分一致条件を付ける
  • 顧客名と会社名の両方が指定された場合、WHERE句に顧客名の部分一致条件と会社名の部分一致条件をANDで結合する

 以下がfind()メソッドの実装例です。

リスト6 find()メソッドの実装例
  public List find(String name, String company) {
        Session session = HibernateUtil.getSession(sessionFactory);
        // パラメータ指定の有無により、検索条件を構築する
        StringBuffer sql = new StringBuffer();
        if (name != null && !name.equals("")) {
            // 顧客名条件ありの場合
            name = "'%" + name + "%'";
            sql.append(" where (c.name.first like ").append(name);
            sql.append(" or c.name.last like ").append(name);
            sql.append(" or c.name.last || c.name.first like ").append(name)
               .append(")");
        }        
        if (company != null && !company.equals("")) {
            // 会社名条件ありの場合
            company = "'%" + company + "%'";
            String prep = sql.length() == 0 ? " where" : " and";
            sql.append(prep);
            sql.append(" c.company.name like ").append(company);
        }
        sql.insert(0, "select c from CustomerBO c join fetch c.company");
        sql.append(" order by c.id asc");
        
        return session.createQuery(sql.toString()).list();
    }

 ここで注意しなければいけない点は、“join fetch”というキーワードです。ここで扱っているドメイン・モデルは、第3回図1で示したもので、会社を表すCompanyBOは顧客CustomerBOのプロパティcompanyによって、関係付けられています。find()メソッドの実行結果は、第3回図2の画面例のように、会社名も一緒に一覧表示しなければなりません。そのため、CustomerBOとCompnayBOをjoinする必要がありますが、“fetch”キーワードも併せて指定する必要があります。

 Hibernateには、遅延ローディング(lazy loading)という機能があり、オブジェクト間の関連を表すプロパティがアクセスされて初めて、テーブル・カラムのデータからオブジェクトへのマッピングを行う場合があります。もし、“fetch”キーワードを省略した場合には、プレゼンテーション層のJSPにモデルが渡されたときに、CustomerBOのgetCompany()メソッドがアクセスされた瞬間に、Hibernate Sessionオブジェクトへのアクセスを試みます。ところが、ビジネス・メソッドのトランザクションは終了しているため、Hibernate Sessionへのアクセスは失敗に終わります。ビジネス層は、プレゼンテーション層に必要なモデルを完全な形でプレゼンテーションに提供する必要があります。そのためには、DAO内の問い合わせ文で“fetch”キーワードを指定して、必要なオブジェクト・ツリーが完全に取得できるようにしなければなりません。

コラム
Hibernateの遅延ローディングに関連して、Sessionの解放をビジネス層のインターセプターではなくServlet Filterで実現し、Sessionが解放されるのをJSPがレンダリングされた後まで意図的に延期する(Open Session in Viewパターン)という考え方があります。

この方法は、複雑なオブジェクト・ツリーを詳細度の異なる複数のページで表示しなければならない場合に、同一の検索式(データアクセス・メソッド)を共有できるというメリットがあります。しかし、Open Session in Viewパターンは、プレゼンテーション層がServletコンテナである場合にしか利用できない点に注意しなければなりません。将来、リッチ・クライアントが直接ビジネス層にアクセスできるようにしなければならなくなったときに、Open Session in Viewを使ったアプリケーションでは、すべてのDAOの問い合わせ文を見直さなければならなくなります。従って、Open Session in Viewはできるだけ利用しないことをお勧めします。

 遅延ローディングと“join fetch”の概念は、EJB 3.0にもそのまま採用されており、リスト6のHQL文はEJB 3.0のEJB-QLと互換性があります。もし、find()メソッドをEntityManagerを使用したものに書き換える場合には、Hibernate Sessionを操作している部分だけをEntityManagerの操作に書き換えれば良いことになります。

リスト7 find()メソッドの実装例
      public List find(String name, String company) {
        EntityManager em = emf.createEntityManager();
        // これ以降は、リスト6のHibernateの場合と同じ
            :
        return em.createQuery(sql.toString()).getResultList();
    }

  1/2

 INDEX

第5回 Hibernateでインテグレーション層のDAOデザインを考える
Page1
EJB 3.0のEntityManager
EntityManagerのライフサイクル
SpringのAOPを用いてHibernate Sessionのライフサイクルを管理する
Hibernateを用いたデータアクセス
  Page2
Hibernate O/Rマッピング:XML vs. アノーテーション


Java Solution全記事一覧



Java Agile フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Java Agile 記事ランキング

本日 月間