ひがやすを技術ブログ

電通国際情報サービスのプログラマ

JPAの問題点

JPAには非常に期待している人も多いでしょう。私もその一人です。実際にプロジェクトで使ってみて、見えてきた問題点を書いてみます。JPAの実装としては、Hibernate3.2を使っています。

  • 学習コストが高い。
    • JPAの全機能のうち、プロジェクトで使うものに絞り込んで教育すると、3日程度で教えることができるのですが、そこそこ使えるようになるには、2〜4週間かかります。これは、Hibernate in Actionにも書いているのでそういうものなんでしょう。
  • トラブルシューティングが難しい。
    • 多くのプロジェクトで実際にハマルのはこれでしょう。うちのプロジェクトでは、Hibernate職人である小林さんがいるにもかかわらずいろいろ苦労しました。Hibernate職人のいないプロジェクトで使うのは厳しいのではないかと思います。
  • SQLの扱いが貧弱。
    • JPQLは、SQLのかなり貧弱なサブセットなので、SQLを使う必要がある場合もいろいろあるのですが、JPASQLの扱いは貧弱です。SQLの結果を単独のEntityにつめて返す場合はいいのですが、複数のEntityを返そうとするとObjectの配列に個々のEntityがつめられて返ってくるので、Employee emp = (Employee) obj[0];のような感じでキャストして使うのです。それってあんまりだよね。最悪は、Entity以外にマッピングする場合で、なんとObjectの配列で返ってくるのです。Long id = (Long) obj[0];String ename = (String) obj[1];なってやる必要があるなんて。超あんまり。KuinaDaoは、S2DaoのようにSQLの結果をDTOマッピングできるので、このケースは大丈夫なのですが、素のJPAを使うときっと困るではないかと思います。
  • 新規システム以外で使うのは難しい。
    • 実際の開発では、本当に新規システムということはあまりなく、既にあるシステムを作り直すケースが多いものです。そのような場合、既にテーブルやSQLは存在するのですが、それをJPAベースに置き換えるのは、大変です。特にSQLをJPQLに変更することと、レガシーなシステムでidが使われていない場合など、かなり大変。
  • パフォーマンスチューニングが後から発生する。
    • JPAを使う場合は、Entity間の関連はlazyにするのが基本です。そうすると最初に少量のデータで開発していたときは問題が起こらないのですが、結合テストのフェーズになって本番に近いデータを扱うようになるとlazy loadingによるN+1 SELECT問題が発生し、パフォーマンスが劣化します。Fetch Joinで対応できればまだ楽なのですが、そうは行かない場合、SQLを書く必要があり、前に書いたSQLの扱いが貧弱な問題にぶち当たります。
  • View層におけるlazy loading問題
    • View層でEntityを直接扱うと、View層でEntityの関連をたどる必要があり、そこでlazy loading(SELECT文の実行)が起こる可能性があります。TransactionスコープなEnityManagerを使う場合、View層でトランザクションを制御する必要があります。それってあんまりじゃねぇ。Seasar2の場合は、S2Dxoを使ってスマートにこの問題を解決できます。
  • 立ち上げ時の初期化が遅い
    • テストするときに、JPAの環境をテストごとに初期化するとあまりに遅くやってられません。Seasar2ではテスティングフレームワーク側で対応していますが、そういうのがないとテストをするのが大変です。
  • 生産性がS2Daoより悪い
    • 既存のSQLを利用できるような開発だと、圧倒的にS2Daoの方が、生産性が上がります。新規に開発する場合でも、JPAの方が複雑なため、トラブルケースが多く意外とコストがかかります。また、書かなければいけないコードの量もJPAの方が圧倒的に多くなります。KuinaDaoを使えば、書かなければいけないコードの量はS2Dao並に少なくなるのですが、複雑だという問題は解決できません。

JPAを使って最も楽をするには、StatefulSessionBeanとExtendedなEntityManagerを使うことです。View層のlazy loadingの問題もないし、detachされたEntityをmergeする必要もないし、SessionBeanが生きている間は、一度アクセスしたEntityはキャッシュされているので、パフォーマンスも向上します。問題点は、一度アクセスしたエンティティをキャッシュしているので、メモリを大量に消費することです。現在のJPAでは、キャッシュを全部クリアするという大雑把なメソッドしか用意されていないので困り者です。また、StatefulSessionBeanをきちんと削除してあげないとキャッシュされたEntityが解放されないと言う問題もあります。これは、Seamでは、StatefulSessionBeanのライフサイクルを自動的に管理するので、解決されているように思えますが、ユーザが最後まで処理を行わずに、途中で処理を止めた場合は、StatefulSessionBeanが削除されずに残ってしまうのではないかと思います。Seamに詳しい人、間違っていたら教えてください。
結論を書くと、今のJPAは、かなりの問題を抱えています。素で使うのはお勧めしません。JPAは、JPAのことを良く知っているフレームワークと組み合わせることをお勧めします。今の時点で、JPAを使うのに良いと思うフレームワークは、Seasar2のEasy EnterpriseファミリーとSeamです。Easy Enterpriseファミリーは、SQLの問題もView層におけるlazy loading問題も解決しているのですが、最大の問題は、S2Hibernate-JPAとKuinaDaoが正式リリースされていないこと。早くリリースしてくださーーい。
Seamは、大量にメモリを使うことが苦にならないようなシステムなら、良いのではないでしょうか。SQLの扱いが貧弱な問題点は、HibernateのSessionを直に呼ぶことで解決できるでしょう。

Uuji(うーじ)

JPAでは、生産性の向上にはつながらないと言うことが分かったので、生産性を向上させる永続化層のフレームワークとして、Uujiを作ることを思いつきました。
当初、Uujiは、極力Javaのクラスもコードも書かずに、データベースにアクセスすることを目指して設計しました。具体的には、規約からSQLを生成し、データベースのやり取りには、Mapを使います。
これは、これで面白いと思ったのですが、Seasarカンファレンス以降、色々ヒアリングしたみて、どうも評判は今ひとつでした(苦笑)。
そこで、もう一度、コンセプトを練り直して再設計することにしました。新コンセプトは、リレーショナルモデル(RDB)をできる限り生かしきることです。JPAドメインモデルを構築することを目的として、SQL(JPQL)の機能を限定しているのと、非常に対照的ではないかと思います。
それでは、S2Daoとの違いは何でしょうか。S2Daoは、SQLを書くことが基本です。SQL文の自動生成もできますが、おまけ程度だと思ったほうが良いでしょう。それに対し、Uujiは、アプリケーションで使う90%以上のSQL文は自動生成することを想定しています。また、1:N、ネストしたN:1の関連もサポートします。N:Nは、1:N,N:1として表現したほうがデータモデルとして明確で良いと思っているのでサポートしません。
また、Uujiは、コードジェネレータ(Dolteng)と相性の良い設計にします。Doltengが最初に吐き出すコードで永続化ロジックの90%以上がカバーできると考えているため、生産性が高いと言われているS2Daoのさらに10倍の生産性をたたき出す可能性もあります。
パフォーマンスもS2Daoよりかなり向上させます。現在のS2Daoでは、最初にアクセスされたときに、テーブルのメタ情報を取得して、Java上では表現されていない情報を補完していますが、このメタデータへのアクセスを止めます。それでは、どこから足りない情報を保管するかというと「規約」です。規約重視をさらに徹底させます。この規約は、プロジェクトごとにカスタマイズできるようにします。さらに、各RDBMSの機能を使い切ることでパフォーマンスの向上を図ります。例えば、Pagingでオラクルならrownumを使うだとかが分かりやすい例です。
それでは、Uujiが完成したときのイメージを見てみましょう。次のようなテーブルがあります。

  • emp
    • emp_id
    • emp_name
    • mgr_id
    • dept_id
    • emp_version
    • emp_inserted_timestamp
    • emp_updateed_timestamp
    • emp_deleted_timestamp
  • dept
    • dept_id
    • dept_name
    • dept_version

Doltengソースコードを自動生成させましょう。
Emp.java, EmpCriteria.java, EmpDao, Dept.java, DeptCriteria.java, DeptDao.javaが自動生成されます。


class Emp {
private Long empId;
private String empName;
private Long mgrId;
private Long deptId;
private Integer empVersion;
private Timestamp empInsertedTimestamp;
private Timestamp empUpdatedTimestamp;
private Timestamp empDeletedTimestamp;
private Emp mgr;
private List emps;
private Dept dept;
...
}
class Dept {
private Long deptId;
private String deptName;
private Integer deptVersion;
private List emps;
...
}
class EmpCriteria extends Emp {
private String orderby;
private Integer firstResult;
private Integer maxResults;
private String[] fetchJoins;
...
}
class DeptCriteria extends Dept {
...
}
interface EmpDao {
Emp findFirst(EmpCriteria criteria);
List findAll(EmpCriteria criteria);
Emp fill(Emp entity, String... fetchJoins);
int count(EmpCriteria criteria);
void insert(Emp entity);
void insertBatch(List entities);
void update(Emp entity);
void updateUnlessNull(Emp entity);
void updateBatch(List entities);
int updateAll(Emp entity, EmpCriteria entity);
void delete(Emp entity);
void deleteBatch(List entities);
int deleteAll(EmpCriteria entity);
}
Criteriaオブジェクトの理解がUujiの肝といっても過言ではありません。具体的な使い方を見ていきましょう。最初は、deptIdが10のものを検索してみましょう。

EmpCriteria criteria = new EmpCcriteria();
criteria.setDeptId(10);
List emps = dao.findAll(criterial);
equals検索は、プロパティに値を入れるだけです。複数のプロパティに値を設定したらand検索になります。この辺は想定どおりでしょう。それでは、deptIdが10と20のものを検索してみましょう。いわゆるinですね。DoltengでEmpCriteriaを右クリックし、Criteriaの設定を選びます。ウィザードが起動するので、deptIdのINのチェックボックスをクリックしてください。OKを押すと次のコードがEmpCriteriaに追加されています。

private Integer[] deptId_IN;
...
それでは検索するコードを書いてみましょう。

EmpCriteria criteria = new EmpCcriteria();
criteria.setDeptId_IN(10, 20);
List emps = dao.findAll(criterial);
deptNameがACCOUNTのものを検索してみましょう。先ほどと同じようにDoltengを呼び出します。deptのツリーをクリックしてノードを開き、deptNameのEQをチェックしてOKボタンを押します。次のコードがEmpCriteriaに追加されています。

private String dept$deptName;
...
それでは検索するコードを書いてみましょう。

EmpCriteria criteria = new EmpCcriteria();
criteria.setDept$deptName("ACCOUNT");
List emps = dao.findAll(criterial);
Uujiは、creteria.fetchJoinsを指定しない限り、関連のエンティティを取得することはしません。それでは、Empと同時にDeptのデータも取得してみましょう。

EmpCriteria criteria = new EmpCriteria();
criteria.setFetchJoins("dept");
List emps = dao.findAll(criterial);
emp.deptにはしっかり値が入っています。FetchJoinのデフォルトは、outerですが、dept innerのようにinner joinにすることもできます。自分の部下も取得してみましょう。部下は名前順に取得します。

criteria.setFetchJoins("dept", "emps orderby emps.empName");
Criteriaのorderbyはソート順を指定するものです。firstResultはPagingのoffset、maxResultはPagingのlimitです。
Daoのfillメソッドは、関連を解決するものです。dao.fill(emp, "dept", "emps orderby emps.empName");Web層でサブミットされたデータには、関連はセットされていないので、業務ロジック側で関連を解決するときに使います。
updateは、データベースから取得したデータと異なる値を持つカラムだけが更新されます。
テーブル名_inserted_timestampのカラムがあれば、データベースの現在時刻(オラクルならsysdate)を自動的に挿入します。テーブル名_updated_timestampも同様です。テーブル名_deleted_timestampは、論理削除用です。テーブル名_deleted_timestampのカラムのあるテーブルでdeleteメソッドが呼び出されるとupdate文でdeleted_timestampが挿入されます。findの時には、SELECT文のWHERE句にdeleted_timestamp == nullの条件が自動的に追加されます。
90%の作業は上記のことを知っていればこなせるでしょう。学習コストが非常に低いことが分かっていただけるのではないでしょうか。