O/Rマッピング・ツールを使った典型的なJ2EEアプリケーションでは最小限のSQL問い合わせで必要なデータをフェッチしなければならないという問題に直面します。しかしこれは簡単ではない場合もあります。デフォルトの設定ではO/Rマッピング・ツールは特別に指定しない限り必要に応じてデータをロードします。この振舞いは遅延ロードと呼ばれ、依存先のデータは要求があった際にのみロードされることを保証し、それによって不要なオブジェクト生成を避けます。扱おうとするビジネス・ユースケースで依存先のコンポーネントが不要な場合には遅延ロードは役立ちますし不要なコンポーネントのロードに関する問題も解決してくれます。
一般的にビジネス・ユースケースはどのようなデータが必要であるのかを知っています。多くのSelectクエリが実行されると、遅延ロードが原因でDBの性能が劣化します。というのもあるビジネス・ユースケースで必要なデータが一度ではフェッチされないからです。多くのリクエストを処理しなくてはならないようなアプリケーションにとってはこのことがボトルネックとなるかもしれません(拡張性の問題)。
では、Person(人物)とAddress(住所)の情報をフェッチする必要があるビジネス・ユースケースの例を見てみましょう。Addressコンポーネントは遅延ロードするように設定されているために必要なデータをフェッチするためにはより多くのSQL問い合わせが発行されます。つまり最初にPersonをその次にAddressを取得するSQLです。これによってデータベースとアプリケーション間の通信を増加させます。今回のビジネス・ユースケースではPersonとAddress両方のコンポーネントが必要であることは分かっているので、一回の問い合わせにより両方を読み込んでしまえばこのような現象を避けることが出来ます。
もしDAO/リポジトリや低レベルのサービス・レイヤにビジネス・ユースケース固有のフェッチ用APIを追加してしまうと同じドメイン・オブジェクトに対して異なるデータをフェッチし初期化するためのAPIを書く必要が生じてきます。これではリポジトリ・レイヤや低レベルのサービス・レイヤが肥大化してしまい、保守の悪夢に陥ってしまいます。
遅延フェッチによる別の問題は必要となる全てのデータがフェッチされるまでデータベースとの接続を保持して置かなければ、アプリケーションから遅延ロード例外が送出されてしまうということです。
注意:問い合わせの際に二次キャッシュにあるデータを熱心にフェッチ(eager fetch)しているとしたら上記の問題に対する解決策をとることは出来ません。O/Rマッピング・ツールのHibernateでは、もし二次キャッシュにあるデータに対して熱心なフェッチ(eager fetch)を使うと例えそのデータが既に二次キャッシュ内に存在してたとしてもキャッシュからではなくデータベースからデータを取得することになるでしょう。Hibernateでは上記の問題に対する解決策はありません。このことは二次キャッシュにあるオブジェクトに対する問い合わせでは決して熱心なフェッチを使うべきではないということを暗示しています。
オブジェクトに対する問い合わせに関するチューニングが出来るO/Rマッピング・ツールをキャッシュを利用するように設定している場合、問い合わせ対象のオブジェクトが既にキャッシュ上に存在していればキャッシュから読み込みますがなければ熱心にフェッチ(eager fetch)します。キャッシュされたデータは(遅延ロードのように)必要に応じて読み込まれるのではなく問い合わせの実行時にフェッチされるので、この仕組みはトランザクションとDB接続に関する上記の問題の解決策となります。
ここで遅延ロードを使った場合に直面する問題とその回避策を見るためのサンプル・コードについて考えてみます。Employee(従業員)、Department(部署)、そしてDependent(扶養家族)という三つのエンティティを含むドメインについて考えます。
これら三つのエンティティ間の関連は以下のようになっています。
- Employeeは0以上のdependentを持つ。
- Departmentは0以上のemployeeを持つ。
- Employeeは0または1つのdepartmentに所属する。
ここで以下の三種類のユースケースを実行する必要があるとします。
- あるemployeeの詳細をフェッチする。
- あるemployeeとそのdependentに関する詳細をフェッチする。
- あるemployeeとその部署に関する詳細をフェッチする。
上記のユースケースはそれぞれ異なるデータがフェッチされ返すことを要求します。遅延ロードを使うと以下のような不利な点があります。
- もしemployeeエンティティに対してdependentエンティティとdepartmentエンティティを遅延ロードするとしたら、ユースケース2と3の場合に必要なデータをフェッチするためにより多くのSQLが発行される。
- 複数回に及ぶ問い合わせサイクルの間ずっとデータベース接続を維持しておかなければ遅延ロード例外が発生するが、そうしておくことは不適切なリソース浪費につながる。
一方で熱心なフェッチ(eager fetch)を使う場合には以下のような不利な点があります。
- employeeに対してdependentとdepartmentを熱心にフェッチ(eager fetch)させると不要なデータまでフェッチすることになる。
- 特定のユースケース向けにチューニングした問い合わせを行うことが出来ない。
上記の問題に対応するためにリポジトリ/DAOや低レベルのサービス・レイヤにユースケース固有のAPIを用意することは以下のような結果をもたらします。
- コードの肥大化 - サービス、リポジトリ/DAOクラスの両方が肥大化する。
- 保守の悪夢 - 新たなユースケースが追加されると、サービス・レイヤやリポジトリ/DAOの両レイヤに対して新しいAPIを追加する必要が生じる。
- コードの重複 - 取得したエンティティに対して低レベルのサービス・レイヤでビジネス・ロジックを適用する必要がある際に発生する。同様にDAO/リポジトリではデータを戻す前にそのデータが有効なものであるかどうかをチェックする必要がある。
上記のような難問を解決するため、リポジトリ/DAOレイヤはユースケースに応じたエンティティを戻すためにどのような問い合わせを行う必要があるのかを知っておく必要があります。このためリポジトリ/DAOクラスのデフォルトのフェッチ方法はアスペクト・クラスによってユースケースに応じたフェッチ方法で上書きします。フェッチ方法を表す全てのクラスは同じインタフェースを実装します。
問い合わせに対するデータを取得するために上で説明したフェッチ方法を使うリポジトリ・クラスは以下のサンプル・コードのような実装になります。
public Employee findEmployeeById(int employeeId) { List employee = hibernateTemplate.find(fetchingStrategy.queryEmployeeById(), new Integer(employeeId)); if(employee.size() == 0) return null; return (Employee)employee.get(0); }
リポジトリ・クラスにあるemployeeのフェッチに関するストラテジ(戦略、方針)はユースケースの要求に応じて変更できる必要があります。リポジトリ・レイヤでどのストラテジを採用すべきかという決定はリポジトリ・レイヤやサービス・レイヤの外に切りだされアスペクト・クラス内で下されます。従って新しいビジネス・ユースケースを追加する際にはアスペクトの修正とリポジトリで使うフェッチング・ストラテジの実装を追加するだけで済みます。ここではビジネス・ユースケースに応じてどのフェッチング・ストラテジを採用すべきかという判断のためにアスペクト指向プログラミングを使います。
アスペクト指向プログラミングとは
アスペクト指向プログラミング(AOP)は実際に多くある横断的な関心事の実装をモジュール化するのを可能にします。ログの取得、トレース、動的なプロファイリング、エラー・ハンドリング、サービス・レベルの契約、ポリシーの適用、プール、キャッシュ、同時実行制御、セキュリティ、トランザクション管理、ビジネス・ルール、などが横断的な関心事の例になります。このような関心事に対する古典的な実装方法では中核をなす関心事と混ざった実装となってしまいます。AOPを使うと、それぞれの関心事をアスペクトと呼ばれる別々のモジュールに実装することが出来ます。このようなモジュール化された実装により、シンプルな設計、分り易さの向上、品質の向上、(アプリケーションの)市場への展開に要する時間の短縮、そしてシステム要求への変更に対する迅速な対応などにつながります。
AspectJのコンセプトとプログラミングについてはRamnivas Laddad氏のAspectJ in Actionを、aspectjツールについてはAspectJ Development Toolsを見て下さい。
フェッチング・ストラテジの実装に当たってアスペクトは非常に大きな役割を担います。どのようなフェッチング・ストラテジを採用すべきかというのはビジネス・ユースケースによって決まるビジネス・レベルの横断的な関心事になります。アスペクトはあるビジネス・ユースケースでどのフェッチング・ストラテジを採用すべきかという判断をするのを助けます。このようにしてフェッチング・ストラテジの決定を管理するのを低レベルのサービス・レイヤやリポジトリ・レイヤから切り離しています。今までと異なるフェッチング・ストラテジを必要とする新たなビジネス・ユースケースが登場しても低レベルのサービスやリポジトリのAPIを修正することなく対応できるのです。
FetchingStrategyAspect.aj
/** Identify the getEmployeeWithDepartmentDetails flow where you need to change the fetching strategy at repository level */ pointcut empWithDepartmentDetail(): call(* EmployeeRepository.findEmployeeById(int)) && cflow(execution(* EmployeeDetailsService.getEmployeeWithDepartmentDetails(int))); /** When you are at the specified poincut before continuing further update the fetchingStrategy in EmployeeRepositoryImpl to EmployeeWithDepartmentFetchingStrategy */ before(EmployeeRepositoryImpl r): empWithDepartmentDetail() && target(r) { r.fetchingStrategy = new EmployeeWithDepartmentFetchingStrategy(); } /** Identify the getEmployeeWithDependentDetails flow where you need to change the fetching staratergy at repository level */ pointcut empWithDependentDetail(): call(* EmployeeRepository.findEmployeeById(int)) && cflow(execution(* EmployeeDetailsService.getEmployeeWithDependentDetails(int))); /** When you are at the specified poincut before continuing further update the fetchingStrategy in EmployeeRepositoryImpl to EmployeeWithDependentFetchingStrategy */ before(EmployeeRepositoryImpl r): empWithDependentDetail() && target(r) { r.fetchingStrategy = new EmployeeWithDependentFetchingStrategy(); }
このようにリポジトリでどの問い合わせを実行すべきであるかという判断はサービス・レイヤやリポジトリ・レイヤから切り離されているので、新しいユースケースのために低レベルのサービス・レイヤやリポジトリ・レイヤを修正する必要はないのです。どの問い合わせを実行するのかという判断ロジックはアスペクトに保持された横断的関心事となります。サービス・レイヤがリポジトリのAPIを実行する直前にビジネス・ユースケースに応じたフェッチング・ストラテジをアスペクトが注入します。このようにして異なるビジネス・ユースケースの要求を満たすためにサービス・レイヤとリポジトリ・レイヤでは一つのAPIを使うことが出来ます。
この点についてemployeeに関するDepartmentとDependentの詳細をフェッチするビジネス・ユースケースの例を見てみましょう。この場合、ビジネス・サービス・レイヤにgetEmployeeWithDepartmentAndDependentsDetails(int employeeId)というユースケースを追加しなければなりません。さらにインタフェースEmployeeFetchingStrategyを実装するEmployeeWithDepartmentAndDependentFetchingStaratergyというフェッチング・ストラテジを示す新しいクラスを実装し、一回の問い合わせで必要なデータを全てフェッチできるように最適化された問い合わせを返すqueryEmployeeByIdというAPIをオーバーライドします。
上記のフェッチング・ストラテジをどのビジネス・ユースケースに注入するのかといった判断は以下のようにアスペクトに配置されます。
pointcut empWithDependentAndDepartmentDetail(): call(* EmployeeRepository.findEmployeeById(int)) && cflow(execution(* EmployeeDetailsService.getEmployeeWithDepartmentAndDependentsDetails(int))); before(EmployeeRepositoryImpl r): empWithDependentAndDepartmentDetail() && target(r) { r.fetchingStrategy = new EmployeeWithDepartmentAndDependentFetchingStaratergy(); }
上記で見たように新しいユースケースのために低レベルのサービス・レイヤやリポジトリ・レイヤを修正する必要はありませんでした。新しいユースケースに対応するために新しいFetchingStrategy実装クラスを作成しアスペクトを使用しただけです。
では第二レベルのキャッシュを利用するように設定されたオブジェクトに対する問い合わせの最適化の問題を見て見ましょう。今回の例ではdepartmentエンティティが二次キャッシュを使うように修正します。もしdepartmentエンティティに対して熱心なフェッチ(eager fetch)を利用すると二次キャッシュに既にデータがあったとしても問い合わせのたびに同じdepartmentをデータベースから取得することになります。もしその問い合わせでdepartmentエンティティをフェッチしないとすると、departmentエンティティはキャッシュにも存在していないため遅延ロードによってフェッチされることになるのでビジネス・レイヤ(ユースケースのレイヤ)はトランザクションの一部となってしまいます。
こうしてビジネス・ユースケースに必要なデータが分っていたとしてもトランザクション境界はより低位のレイヤからビジネス・レイヤにまで引き上げられます。但しO/Rマッピング・ツールが上記の問題(キャッシュ・データに対する熱心なフェッチ(eager fetch)の問題)を解決する仕組みを提供していない場合にはそれが出来ません。
上述の方法は全てのキャッシュされていないデータに対して上手く機能しますが、キャッシュされたデータについてはそれを扱うO/Rマッピング・ツール次第で機能しないこともあります。
フェッチング・ストラテジを説明した動作する例を確認するには添付されたソースコード全体を見て下さい。zipファイルには上で述べたの全てのシナリオを説明した動作する例が含まれています。どのようなIDEからでも、またはコマンド・プロンプトからでもaspectjのコンパイラを使って添付のソースコードを実行し試すことが出来ます。実行する前にjdbc.propertiesを修正しデモ・アプリケーションが必要とするデータベースを作成してあることを確認してください。
Eclipse IDEとAJDTプラグインを使った場合の手順は以下のようになります。
- ダウンロードしたソースコードを解凍しeclipseにインポートする。
- Resources/dbscript以下にあるjdbc.propertiesに書かれているデータベースの設定を修正する。
- 前のステップで設定したデータベースに対してresources/dbscript/tables.sqlを実行しデモ・アプリケーションに必要なテーブルを作成する。
- Main.javaをAspectJ/Javaアプリケーションとして実行して初期データを作成し上記のフェッチング・ストラテジの実装を試す。
結論
この記事では低レベルのサービス・レイヤやリポジトリ・レイヤを肥大化させることなく、フェッチング・ストラテジによってモジュール化された方法でバックエンドにあるシステムからデータを取得する処理を最適化する方法について示しました。
略歴:Manjunath R Naganna氏はAlcatel LucentにてSenior Software Engineerとして勤務しておりJava/J2EEを使ったエンタープライズ・アプリケーションの設計及び実装を専門にしています。特にSpringフレームワーク、ドメイン駆動設計、イベント駆動アーキテクチャ、そしてアスペクト指向プログラミングに興味があります。Manjunath氏は記事の編集および構成を担当したHemraj Rao Surlu氏に感謝しています。さらに仕事上のリーダーでありレビューと貴重なフィードバックをしたRamana氏とSaurabh Sharma氏にも感謝しています。