JavaEE7をはじめよう(6) - JPAクエリ(その3) Criteria API
前々回と前回の記事で、JPAが提供する3つのクエリの定義方法のうち、JPQLとNaitiveQueryについて解説した。 今回は、最後の1つであるCriteria APIについて解説する。
Criteria API
Criteria API は、API呼び出しによってクエリオブジェクトを構築する方法で、JPQLで定義されている文法と同じ内容のクエリを作成できる。 型安全なAPI呼び出しを使ってクエリを組み立てるため、動的にクエリを組み立てる必要がある場合には便利な仕組みである。
ただし、型安全であることにこだわったためか、 Criteria APIは使用するクラスが多く、非常に複雑な仕組みになっている。
Criteria APIで使用するクラスは主に以下の4つである。
CriteriaQuery
クエリ全体に該当し、select
句、from
句、where
句、group by
句などを設定し、クエリ全体を生成する。CriteriaBuilder
1つの式(select
句の1項目、where
条件の1つの条件、order by
句の1つのオーダー指定など)を生成する。Root
エンティティの列に関する情報を取得するためのクラス。Metamodel
エンティティクラスごとの列の型の情報を設定してあるクラス。各エンティティごとに自動生成される。
単一エンティティのクエリ
以下のJPQLがあるとする。
select m from Member m where m.name like :name and m.playerNumber = :num orderBy m.id;
ここでname
とnum
が引数に指定されなかった場合に、対応する条件を除外したいとしよう。そのような場合には、Criteria APIを用いることでうまく制御できる。
まず、EntityManager#getCriteriaBuilder
を呼び出してCriteriaBuilder
を生成し、そこからCriteriaQuery
とRoot
を生成する。
// EntituManager(変数em)から、CriteriaBuilderの取得 CriteriaBuilder cb = em.getCriteriaBuilder(); // CriteriaQueryの生成。型はselect句に指定する内容とあわせる。 CriteriaQuery<Member> query = cb.createQuery(Member.class); // Rootの生成。Rootは from句に記載するエンティティを指定して生成する。 Root<Member> r = query.from(Member.class);
次にselect
句を指定する。
// select句にエンティティを設定するなら、rootを渡せばよい。
query.select(r);
次にwhere
句を指定する。
// 引数name, numberの状態によって条件の生成を行う。 List<Predicate> preds = new ArrayList<>(); if (name != null) { // like条件 preds.add(cb.like(r.get(Member_.name), name + "%")); } if (number != null) { // = 条件 preds.add(cb.equal(r.get(Member_.playerNumber), number)); } // 上記の条件をand条件で設定。 query.where(cb.and(preds.toArray(new Predicate[]{})));
where
句の各条件を作るには、CriteriaBuilder
の like
, equal
, between
などのメソッドを用いる。
これらのメソッドの戻り値は、SQLの式の条件に相当Predicate
クラスのオブジェクトである。
それぞれのメソッドの第一引数には、エンティティのカラムに対応する値(JPQLで、m.name
のように書くことと同様)を指定する必要がある。
上記のサンプルコードでは、エンティティのカラムに対応する値を取得するためにRoot#get(Member_.xxx)
と記述している。
このMember_
のような エンティティ名の後ろにアンダースコアが付くクラスは、ビルド時に自動生成されるメタモデルといわれるクラスである。
メタモデルにエンティティごとのカラム名に対応するフィールドを持たせ、文字列の代わりにメタモデルのフィールドを用いることで、カラム名のスペルミスを防止できる。 また、カラム名が変更された場合にはコンパイルエラーとなるので、型安全にクエリを組み立てることができる。
なお、メタモデルは必須ではなく、文字列によって列名を指定することもできる。その方法を採用してカラム名を間違えた場合には、コンパイルエラーとならず実行時エラーになる。
続けてorder by
句を指定する。
query.orderBy(cb.asc(r.get(Member_.id)));
最後にクエリを生成する。クエリはEntityManger#createQuery
で作成する。
List<Member> list = em.createQuery(query).getResultList();
ここで生成されたクエリはQuery
のインスタンスなので、JPQLの記事で紹介したページング制御(取得開始位置指定、件数指定)などの操作も行うことができる。
JOINや複雑なクエリの使用
JPQLで紹介した、チームごとの人数をコンストラクタ式で取得する例を、Criteria API で実装してみよう。
作成するのは、JPQLの記事で取り上げたサンプルと同じで、以下のクエリーとする。
select new sample.dto.TeamSummary(t.name, count(t)) from Member m join Team t on m.belongs = t group by t
JOIN結果を格納するサマリークラスは以下の通り。
package sample.dto; public class TeamSummary { private String teamName; private long memberCount; public TeamSummary(String teamName, long count){ this.teamName = teamName; this.memberCount = count; } public TeamSummary(){} // 以下、getter,setterが続く
以下がサンプルである。
CriteriaBuilder cb = em.getCriteriaBuilder(); // クエリの結果型を指定する CriteriaQuery<TeamSummary> query = cb.createQuery(TeamSummary.class); Root<Member> r = query.from(Member.class); // JOINの実行 : Member m join Team t on m.belongs = t と同じ。 Join<Member, Team> join = r.join(Member_.belongs); // 結合された方のテーブルの列を取得する場合、上記の変数joinを使用する // コンストラクタ式、count呼び出しに対応するメソッドをCriteriaBuiderで呼び出す query.select(cb.construct(TeamSummary.class, join.get(Team_.name), cb.count(join))) .groupBy(join.get(Team_.id), join.get(Team_.name)); List<TeamSummary> res = em.createQuery(query).getResultList();
このコードの特徴的な点は2つある。
1点目は、JOINを行っていることである。
JOINは、Root
クラスのjoin
メソッドを用いて結合対象のエンティティの列をメタモデルで指定している。
ここで得られたJoin
クラスは、結合先エンティティTeam
の情報を保持している。
(前掲のJPQLで言えば、Member m join Team t
の中の m
がRoot
で、t
がJoin
に相当する。)
このためselect
句やwhere
句などで結合先のエンティティの列を指定する場合には、Root
ではなく、Join
クラス経由で行う必要がある。
(コード後半のQuery
オブジェクトに対するselect
やgroupBy
メソッド呼び出しの引数を参照のこと)。
2点目はselect
句で列指定を行っていることである。
エンティティの全項目を取得する場合は、Root
インスタンスを渡せばよい(select m
と書くのと同義)が、列指定(select m.id, m.name
など)を行う場合や関数(select max(m.number)
など)を使う場合は、select
メソッドにRoot
から取得した列や、CriteriaBuilder
から関数呼び出しに対応する式を生成する必要がある。
上記のコードでは、コンストラクタ式に対応するconstruct
メソッドを用いているが、他にも max
やmin
など、JPQLで使用可能な関数などが提供されている。
このように、結合やエンティティ以外のselect
文であっても、Criteria API で実現できる。
3つのクエリ実行方法のまとめ
以上、3回にわたって、クエリを実行する3つの方式について紹介してきた。
私見ではあるが、以下の理由によりクエリは原則的にJPQLを使用するべきだろう。
- NativeQuery は JPAが内部で行っているエンティティとデータベースの対応付けを考慮する必要がある。
- NativeQuery は JPQLと比べて制約が多い。
- Criteria API はお世辞にも使いやすいとは言い難い。
ここで検討が必要になるのは、動的なクエリをどうやって実現するかだろう。 JPQLを使用して文字列で組み立てるのも、 Criteria API で組み立てるのもどちらも煩雑になってしまうからである。
プロジェクトの規模やメンバーの習熟度を考慮して、簡単に動的クエリを組み立てる仕組みを導入した方がよいだろう。 方法としては以下の2つが考えられる。
前者については、JPAで使用可能なライブラリは現状見当たらなかった。
後者については、Querydsl というライブラリの中に、Querydsl JPA が存在する。
次回は、JPAクエリの番外編として、Querydsl JPAを紹介する。
[前多 賢太郎]