第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 { |
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) |
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 { |
HibernateUtilクラスでは、同一リクエスト内で一度取得されたSessionオブジェクトが再利用されるように、ThreadLocalを用いて取得済みのSessionオブジェクトを管理しています。また、複数のSessionに対応するため、ThreadLocalはMapとして管理していることに注意してください。
ビジネス・ロジックの最後には、使用されたSessionをすべてclose()し、解放する必要があるため、HibernateUtilクラスにこのためのメソッドcloseAllSessions()を設けます。
public static void closeAllSessions() { |
このcloseAllSessions()は、HibernateInterceptorクラスによって、自動的にすべてのビジネス・メソッドで実行されるようにします。HibernateInterceptorの実装は、ポイントカットのafter adviceとして、closeAllSessions()を呼ぶだけのシンプルなものです。
リスト3 HibernateInterceptorクラス class HibernateInterceptor implements MethodInterceptor { |
|
Hibernateを用いたデータアクセス |
上述の2つのユーティリティクラスを用意することにより、Hibernateによるデータアクセスのコードは、EntityManagerを使用した場合と同程度にシンプルなものになります。例えば、以下は、指定された顧客IDに対応する顧客情報をテーブルから削除するCustomerDAOのdelete()メソッドの実装例です。
リスト4 CustomerDAOImplクラスの実装例public class CustomerDAOImpl implements CustomerDAO { |
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使用)
|
同様に、顧客名と会社名をキーにして、条件に一致する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) { |
ここで注意しなければいけない点は、“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) { |
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全記事一覧 |
- 実運用の障害対応時間比較に見る、ログ管理基盤の効果 (2017/5/9)
ログ基盤の構築方法や利用方法、実際の案件で使ったときの事例などを紹介する連載。今回は、実案件を事例とし、ログ管理基盤の有用性を、障害対応時間比較も交えて紹介 - Chatwork、LINE、Netflixが進めるリアクティブシステムとは何か (2017/4/27)
「リアクティブ」に関連する幾つかの用語について解説し、リアクティブシステムを実現するためのライブラリを紹介します - Fluentd+Elasticsearch+Kibanaで作るログ基盤の概要と構築方法 (2017/4/6)
ログ基盤を実現するFluentd+Elasticsearch+Kibanaについて、構築方法や利用方法、実際の案件で使ったときの事例などを紹介する連載。初回は、ログ基盤の構築、利用方法について - プログラミングとビルド、Androidアプリ開発、Javaの基礎知識 (2017/4/3)
初心者が、Java言語を使ったAndroidのスマホアプリ開発を通じてプログラミングとは何かを学ぶ連載。初回は、プログラミングとビルド、Androidアプリ開発、Javaに関する基礎知識を解説する。
|
|