エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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;

ここでnamenumが引数に指定されなかった場合に、対応する条件を除外したいとしよう。そのような場合には、Criteria APIを用いることでうまく制御できる。

まず、EntityManager#getCriteriaBuilderを呼び出してCriteriaBuilderを生成し、そこからCriteriaQueryRootを生成する。

// 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句の各条件を作るには、CriteriaBuilderlike, 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の中の mRootで、tJoinに相当する。)

このためselect句やwhere句などで結合先のエンティティの列を指定する場合には、Rootではなく、Joinクラス経由で行う必要がある。 (コード後半のQueryオブジェクトに対するselectgroupByメソッド呼び出しの引数を参照のこと)。

2点目はselect句で列指定を行っていることである。

エンティティの全項目を取得する場合は、Rootインスタンスを渡せばよい(select mと書くのと同義)が、列指定(select m.id, m.nameなど)を行う場合や関数(select max(m.number)など)を使う場合は、selectメソッドRootから取得した列や、CriteriaBuilderから関数呼び出しに対応する式を生成する必要がある。
上記のコードでは、コンストラクタ式に対応するconstructメソッドを用いているが、他にも maxminなど、JPQLで使用可能な関数などが提供されている。

このように、結合やエンティティ以外のselect文であっても、Criteria API で実現できる。

3つのクエリ実行方法のまとめ

以上、3回にわたって、クエリを実行する3つの方式について紹介してきた。

私見ではあるが、以下の理由によりクエリは原則的にJPQLを使用するべきだろう。

  • NativeQuery は JPAが内部で行っているエンティティとデータベースの対応付けを考慮する必要がある。
  • NativeQuery は JPQLと比べて制約が多い。
  • Criteria API はお世辞にも使いやすいとは言い難い。

ここで検討が必要になるのは、動的なクエリをどうやって実現するかだろう。 JPQLを使用して文字列で組み立てるのも、 Criteria API で組み立てるのもどちらも煩雑になってしまうからである。

プロジェクトの規模やメンバーの習熟度を考慮して、簡単に動的クエリを組み立てる仕組みを導入した方がよいだろう。 方法としては以下の2つが考えられる。

  • MyBatisのようなテンプレートSQLによる生成
  • Criteria API をラップし簡単に扱える DSLの提供

前者については、JPAで使用可能なライブラリは現状見当たらなかった。

後者については、Querydsl というライブラリの中に、Querydsl JPA が存在する。

次回は、JPAクエリの番外編として、Querydsl JPAを紹介する。

[前多 賢太郎]