JPAを深掘りするシリーズの応用編です。
前回はCriteria APIの基本と利点をご紹介しました。今回はCriteria APIの実戦的な機能をいろいろとご紹介します。
準備・エンティティクラスとメタモデルクラスのサンプル
次のようなエンティティクラスとメタモデルクラスが存在することを前提に進めていきます。
まずはUserエンティティクラスです。
package sandbox.entity; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; @Column(length = 150, unique = true, nullable = false) String name; // getter/setterは省略 }
次にUserエンティティのプロパティの型を表すメタモデルクラスです。
package sandbox.entity; import javax.annotation.Generated; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.StaticMetamodel; @Generated(value="Dali", date="2016-02-12T22:59:19.623+0900") @StaticMetamodel(User.class) public class User_ { public static volatile SingularAttribute<User, String> name; public static volatile SingularAttribute<User, Long> id; }
それではCriteria APIを見ていきましょう。
Where条件にメタモデルクラスを使う
メタモデルクラスは、Criteria APIによるクエリの型安全性を高めるという重要な役割を持っています。
冒頭で示したUserというエンティティクラスをnameプロパティの値で絞り込むクエリを作ってみましょう。
private static void selectUsersByName(final EntityManager em, final String pUserName) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<User> query = builder.createQuery(User.class); final Root<User> root = query.from(User.class); query.where( // // メタモデルクラスを使ってカラムを指定する builder.equal(root.get(User_.name), pUserName) // ); final List<User> result = em.createQuery(query).getResultList(); for (final User user : result) { System.out.println(user); } }
eclipseの入力補完の様子を見ると、fromに指定したエンティティクラスに応じたメタモデルクラスが候補として表示されることが分かると思います。
Where句に複雑な条件を設定する
検索画面のようにWhere句が動的かつ複雑になる時に、Criteria APIは威力を発揮します。例えば入力のある条件はWhere句に加えるが、入力のない条件はWhere句に加えない、という処理をJPQLで書くのは割と大変です。
private static void selectUsersByName(final EntityManager em, final String name) { final StringBuilder s = new StringBuilder("select e from User e where 1 = 1"); if (name != null) { s.append(" and e.name = :name"); } final TypedQuery<User> query = em.createQuery(new String(s), User.class); if (name != null) { // 存在しないパラメータを設定すると例外が生じるため、再度チェックせざるを得ない! query.setParameter("name", name); } final List<User> result = query.getResultList(); for (final User user : result) { System.out.println(user); } }
これをCriteria APIで書き直すと、とても素直なプログラムになります。
private static void selectUsersByName(final EntityManager em, final String name) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<User> query = builder.createQuery(User.class); final Root<User> root = query.from(User.class); final List<Predicate> where = new ArrayList<>(); if (name != null) { where.add(builder.equal(root.get(User_.name), name)); } query.where(where.toArray(new Predicate[where.size()])); final List<User> result = em.createQuery(query).getResultList(); for (final User user : result) { System.out.println(user); } }
関連クラスを同時に取得する
JPAの強みの1つにエンティティクラスの関連をコードで表現出来ることがあります。下記はUserを親に持つTagというエンティティを作っています。
package sandbox.entity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; @Entity public class Tag { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; @Column(length = 50, unique = false, nullable = false) String name; @ManyToOne(fetch = FetchType.LAZY) User user; @Column(nullable = false) long starCount; // getter/setterは省略 }
Tagを取得する際、親のUserも同時に取ってくるようなクエリを作ってみましょう。
private static void selectTagWithUser(final EntityManager em) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<Tag> query = builder.createQuery(Tag.class); final Root<Tag> root = query.from(Tag.class); root.fetch(Tag_.user, JoinType.LEFT); // 関連エンティティを同時に取ってくるように指定 final List<Tag> result = em.createQuery(query).getResultList(); for (final Tag tag : result) { System.out.println(tag.getUser()); } }
9行目がポイントです。実はこの行がなくても関連エンティティは取得できるのですが、SQLの実行回数が格段に変わってきます。俗に言う「N+1問題」を回避する手法です。
関連先のエンティティの条件を指定する
JPAの強力な機能の1つに、関連先エンティティの条件指定がとても簡単な点があります。
private static void selectTagByUserName(final EntityManager em, final String pUserName) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<Tag> query = builder.createQuery(Tag.class); final Root<Tag> root = query.from(Tag.class); query.where(builder.equal(root.get(Tag_.user).get(User_.name), pUserName)); final List<Tag> result = em.createQuery(query).getResultList(); for (final Tag tag : result) { System.out.println(tag); } }
6行目がポイントです。「Tagクラス#userプロパティ」→「Userクラス#nameプロパティ」とたどって条件指定しています。SQLで同じことをしようとするとJOINを書く必要があり手間がかかるところですが、JPAではかなりシンプルに書けることが分かります。
Where句に IN条件を指定する
IN条件は少し特殊な書き方をするのでハマることがあります。
private static void selectUsersByNames(final EntityManager em, final String... names) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<User> query = builder.createQuery(User.class); final Root<User> root = query.from(User.class); if (names != null && names.length > 0) { query.where(root.get(User_.name).in(names)); } final List<User> result = em.createQuery(query).getResultList(); for (final User user : result) { System.out.println(user); } }
7行目でIN条件を指定しています。CriteriaBuilderを使わない点に注意してください。
また要素数が0の時の挙動は、残念ながらDB製品依存となるようなので、プログラムで明示的にチェックするのが無難でしょう。
Tupleを使って柔軟な結果を得る
Tupleという汎用クラスを使えばエンティティにとらわれない形でデータを取得できます。例えば、JOINを使って複数のテーブルからデータを取得しつつ集計するような用途に有用です。
ただし、型安全性が失われる点には注意が必要です。
private static void selectTupleWithJoin(final EntityManager em) { final CriteriaBuilder builder = em.getCriteriaBuilder(); // Tupleで結果を受け取る final CriteriaQuery<Tuple> query = builder.createTupleQuery(); // Tagを主テーブルにする final Root<Tag> root = query.from(Tag.class); // JOINする final Join<Tag, User> join = root.join(Tag_.user); final Path<String> userName = join.get(User_.name); // ユーザ名で集計する query.groupBy(userName); // Userから特定のカラムのみ取得しつつTagのstarCountを合計する // 後で取り出しやすいように、カラムには別名を付けておく query.multiselect( // userName.alias("user_name") // , builder.sum(root.get(Tag_.starCount)).alias("tag_count") // ); final List<Tuple> result = em.createQuery(query).getResultList(); for (final Tuple e : result) { System.out.println(e.get("user_name") + " " + e.get("tag_count")); } }
任意のデータクラスをクエリの結果の型にする
Tupleの弱点は、Tupleが汎用的すぎて「どんなデータがいくつ入っているか分かりにくい」ことです。これを補うために、任意の型をクエリの結果とすることができます。
Tupleの例を書き直してみます。まずは結果を受け取るためのクラスを作ります。コンストラクタで全ての値を受け取るようにする必要があります。
public class StarSummary { private final String userName; private final long tagCount; public StarSummary(final String pUserName, final long pTagCount) { this.userName = pUserName; this.tagCount = pTagCount; } // getterは省略 }
作ったクラスをクエリの結果とするには、CriteriaBuilder#construct(Class, Selection<?>...)
を使ってクエリを組み立てます。
private static void selectStartSummary(final EntityManager em) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<StarSummary> query = builder.createQuery(StarSummary.class); final Root<Tag> root = query.from(Tag.class); final Join<Tag, User> join = root.join(Tag_.user); query.select( // builder.construct(StarSummary.class // , join.get(User_.name) // , builder.sum(root.get(Tag_.starCount)) // )); query.groupBy(join.get(User_.name)); final List<StarSummary> result = em.createQuery(query).getResultList(); for (final StarSummary e : result) { System.out.println(e); } }
9〜11行目でデータクラスを扱っています。
Tupleに比べ扱いやすい型で結果を得られますが、一方でコンストラクタ引数の順番に依存するプログラムとなってしまうのが弱点です。ですがTupleの「どんなデータがいくつ入っているか分かりにくい」という欠点はとても重大ですので、できれば、こちらのデータクラスを使う方が望ましいです。
型安全になりきれていない箇所
ここまで見てきたように、Criteria APIは型安全にクエリを組み立てられる非常に便利な機能なのですが、残念ながら型安全になりきれていない箇所があります。代表的なのはCriteriaBuilder#equal(Expression<?> exp, Object value)
メソッドです。
例えば次のコードを見てください。
query.where( // builder.equal(root.get(User_.name), pUserName) // );
これを次のように書き間違えたとしても、コンパイルエラーにはならないのです。
query.where( // builder.equal(root.get(User_.id), pUserName) // );
このケースでは実行時例外がスローされますが、場合によっては例外がスローされず普通に実行されてしまうため、分かりにくいバグの原因となる可能性があります。
このメソッドのシグニチャが
<C> Predicate equal(Expression<C> exp, C value)
となっていればよかったのに・・・と思えてなりません。頻繁に使うメソッドだけに残念です。
JPAのその他の機能
最後に、ちょっとした便利機能をいくつか紹介しておきます。
ページングに使える2つのメソッド
Webアプリケーションでは検索結果が大量になる場合、ページング表示するのが普通です。JPAには簡単にページングを実装するためのメソッドがあります。
setFirstResult()/setMaxResults()
です。
private static List<User> selectUsersWithPaging(final EntityManager em, final int pPageIndex, final int pCountPerPage) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<User> query = builder.createQuery(User.class); query.from(User.class); return em.createQuery(query) // .setFirstResult(pPageIndex * pCountPerPage) // .setMaxResults(pCountPerPage) // .getResultList(); }
7行目と8行目でページングを指定しています。
結果が必ず1件と分かっている場合に使えるメソッド
結果が必ず1件と分かっている時にはgetSingleResult()
メソッドが使えます。
private static void countAllUsers(final EntityManager em) { final CriteriaBuilder builder = em.getCriteriaBuilder(); final CriteriaQuery<Long> query = builder.createQuery(Long.class); final Root<User> root = query.from(User.class); query.select(builder.count(root)); final long count = em.createQuery(query).getSingleResult().longValue(); System.out.println(count); }
ただし、結果が1件でなかった時は例外がスローされますので、絶対に1件と確信が持てる場合にのみ使ってください。
まとめ
Criteria APIを中心に、2回に渡ってJPAの様々な機能を解説してきましたがいかがでしたでしょうか。
JPAには他にも便利で重要な機能がたくさんあります。特にエンティティクラスの設計は工夫のしがいがあり、別の機会でご紹介したいところです。
非常にパワフルなJPA。JavaEEを使う場合はぜひJPAを使って効率的かつ安全にDBアクセスを実装していただきたいと思います。