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が自動生成されます。
Criteriaオブジェクトの理解がUujiの肝といっても過言ではありません。具体的な使い方を見ていきましょう。最初は、deptIdが10のものを検索してみましょう。
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 Listemps;
private Dept dept;
...
}
class Dept {
private Long deptId;
private String deptName;
private Integer deptVersion;
private Listemps;
...
}
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);
ListfindAll(EmpCriteria criteria);
Emp fill(Emp entity, String... fetchJoins);
int count(EmpCriteria criteria);
void insert(Emp entity);
void insertBatch(Listentities);
void update(Emp entity);
void updateUnlessNull(Emp entity);
void updateBatch(Listentities);
int updateAll(Emp entity, EmpCriteria entity);
void delete(Emp entity);
void deleteBatch(Listentities);
int deleteAll(EmpCriteria entity);
}
equals検索は、プロパティに値を入れるだけです。複数のプロパティに値を設定したらand検索になります。この辺は想定どおりでしょう。それでは、deptIdが10と20のものを検索してみましょう。いわゆるinですね。DoltengでEmpCriteriaを右クリックし、Criteriaの設定を選びます。ウィザードが起動するので、deptIdのINのチェックボックスをクリックしてください。OKを押すと次のコードがEmpCriteriaに追加されています。
EmpCriteria criteria = new EmpCcriteria();
criteria.setDeptId(10);
Listemps = dao.findAll(criterial);
それでは検索するコードを書いてみましょう。
private Integer[] deptId_IN;
...
deptNameがACCOUNTのものを検索してみましょう。先ほどと同じようにDoltengを呼び出します。deptのツリーをクリックしてノードを開き、deptNameのEQをチェックしてOKボタンを押します。次のコードがEmpCriteriaに追加されています。
EmpCriteria criteria = new EmpCcriteria();
criteria.setDeptId_IN(10, 20);
Listemps = dao.findAll(criterial);
それでは検索するコードを書いてみましょう。
private String dept$deptName;
...
Uujiは、creteria.fetchJoinsを指定しない限り、関連のエンティティを取得することはしません。それでは、Empと同時にDeptのデータも取得してみましょう。
EmpCriteria criteria = new EmpCcriteria();
criteria.setDept$deptName("ACCOUNT");
Listemps = dao.findAll(criterial);
emp.deptにはしっかり値が入っています。FetchJoinのデフォルトは、outerですが、dept innerのようにinner joinにすることもできます。自分の部下も取得してみましょう。部下は名前順に取得します。
EmpCriteria criteria = new EmpCriteria();
criteria.setFetchJoins("dept");
Listemps = dao.findAll(criterial);
Criteriaのorderbyはソート順を指定するものです。firstResultはPagingのoffset、maxResultはPagingのlimitです。
criteria.setFetchJoins("dept", "emps orderby emps.empName");
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%の作業は上記のことを知っていればこなせるでしょう。学習コストが非常に低いことが分かっていただけるのではないでしょうか。