JavaEE7をはじめよう(4) - JPAクエリ(その1) JPQL
今回から3回に分けて、JPAでエンティティを検索する手段であるクエリを解説する。
JPA では javax.persistence.Query
というオブジェクトで、問い合わせの内容を表す。
このQuery
を作成する方法は以下の3種類がある。
いずれの場合もEntityManager
にあるそれぞれのクエリを作成するメソッドを呼び出してQuery
を生成する。
以降、それぞれの定義方法や特徴を記述するが、今回の記事ではJPQLについて解説し、Native QueryとCriteria APIに関しては次回以降で解説する。
JPQLとは
JPQL(Java Persistence Query Language)は、JPAが標準化したもので、SQLに似た問い合わせ言語である。
データベース製品ごとのSQL文法の違いを吸収するため、移植性が高い。また、エンティティに定義されている関連のナビゲーションなど、豊富な機能を提供している。 このため、JPAを利用する場合に採用するクエリ手段の第一候補となるだろう
JPQLの文法
詳細はこちらを参照のこと。
前回までのサンプルに含まれるMember
エンティティに対して、背番号が2以上の選手を取得するクエリを書くと次のようになる(サンプルに関してはこの連載の第2回記事を参照のこと)。
select m from Member m where m.playerNumber > 2
from
句にはMember m
のように、エンティティ名とそのエンティティのエイリアスを記述し、where
句などではこのエンティティのプロパティを使って条件を指定する(SQLと異なりプロパティ名のみの記述はできず、m.playerNumber
のようにエイリアスを付与する必要がある)。
またselect
句にエンティティを指定すると、エンティティの全プロパティが取得され(select *
に近い)、クエリの結果はエンティティのインスタンスとして返される。
select
句には、エンティティ以外にも個別のプロパティや集計関数も記述できる(結果のマッピングは後述する)。
select max(m.playerNumber), m.name from Member m
結合もサポートする。
その場合、join
または left join
に続けてfrom
と同じように、エンティティ名とエイリアスを書いておく。
select m from Member m join Team t on m.belongs.id = t.id where m.playerNumber > 1 order by t.name
結合条件は on
句に記載可能だ(on
句はJava EE7からサポートされている。on
句ではなく、where
句に書いても問題ない)。
ちなみに、JPQLでは内部結合と左外部結合のみをサポートする。
次の例のように、結合条件にキーではなくエンティティを指定することも可能だ。
select m from Member m join Team t on m.belongs = t where m.playerNumber > 1 order by t.name
更に、エンティティに対して、アノテーションで関連を指定してあれば、結合条件がなくとも関連を辿ることができる。
select m from Member m where m.playerNumber > 1 order by m.belongs.name
上の3つのJPQLは、ほぼ同じSQLに翻訳される。
JPQLの定義と使用方法
次に、JPQLをどのように定義し、使用するかを見ていこう。
まず定義方法には、以下の2つがある。
- NamedQuery(名前つきクエリ)による定義
- 文字列による定義
JPQLの使用方法(1) - NamedQuery
NamedQueryは静的なクエリを定義する手段で、主に@NamedQuery
アノテーションで指定する(説明は割愛するが、xmlファイルとして定義することも可能だ)。
このNamedQueryは、アプリケーション起動時にJPQLの解析を行い、翻訳されたSQLをキャッシュするため、効率面で有利である。
定義方法としては、エンティティごとのクエリをエンティティクラスに指定するのが一般的だ。
下記の例では、Member.byName
, Member.byNumberRange
という名前の2つのクエリを定義している。
クエリ名はアプリケーション全体で一意にする必要があるので、エンティティ名などを付与すると良いだろう。
@Entity @Table(uniqueConstraints = @UniqueConstraint( columnNames = {"PLAYER_NUMBER", "belongs_id"})) @NamedQueries({ @NamedQuery(name = "Member.byName", query = "Select m from Member m where m.name = ?1"), @NamedQuery(name = "Member.byNumberRange", query = "Select m from Member m where m.playerNumber between " + ":from and :to AND not m.playerNumber in :excludes") }) public class Member implements Serializable { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; //省略
上記のJPQLの中の、 ?1
や :from
などは、クエリの可変なパラメータを表現するバインド変数である。
バインド変数の詳細は次節で解説するが、?
はインデックス指定、 :
はパラメータ名の文字列指定を意味する(1つのクエリの中に両者は混在できない)。
特別な理由がなければ、後者の文字列指定のほうがわかりやすいだろう。
NamedQueryの実行
NamedQueryの実行方法は以下のようになる。
// 単一結果取得クエリの結果をOptionalにマップする関数 Function<String, Optional<Member>> getByOption = name -> { try { // NamedQueryの実行、インデックスによるパラメータ指定、単一取得 Member m = em.createNamedQuery("Member.byName", Member.class) .setParameter(1, name) .getSingleResult(); return Optional.of(m); } catch(NoResultException e){ // getSignglResultは結果が無い場合、NoResultExceptionを投げる。 return Optional.empty(); } }; // name1という選手を検索し、存在しない場合は空のMemberインスタンスを得る。 Member player = getByOption.apply("name1").orElse(new Member()); // 複数件取得クエリの実行、文字列によるパラメータ指定。 List<Member> r2 = em.createNamedQuery( "Member.byNumberRange", Member.class) .setParameter("from", 1) .setParameter("to", 12) // IN句にはリストを渡せる。 .setParameter("excludes", Arrays.asList(3,5,10)) // 取得開始位置や件数の指定。 .setFirstResult(1).setMaxResults(10) .getResultList();
クエリ条件の指定
上記のコードの5行目で、EntityManager
の createNamedQuery
メソッドを呼び出す際に、Member.byName
という名前のNamedQueryを指定している。
第2引数に指定しているのはクエリ実行結果を格納するクラスである。第2引数を指定することで結果オブジェクトのキャストが不要になる。
createNamedQuery
に限らず、EntityManager
の createxxxQuery
系メソッドはjavax.persistence.Query
を実装したインスタンスを返す。
Query
に対する設定は、上記にあるようにメソッドチェーンで記述できる。
setParameter
はバインド変数を設定するメソッドであり、インデックス指定なら数値を、文字列指定ならJPQLに記述した変数名(:は除く)を指定し、第2引数には設定値を記述する。
特筆すべきは、IN
句にリストなどの可変値を直接渡せることだろう。こうした値の渡し方は、通常のプリペアドステートメントでは不可能だが、この記述をすればリストの要素数に応じたSQLが生成される。
setFirstResult
やsetMaxResults
で複数結果の取得開始件数や取得件数を指定することができる。
これらのメソッドを実行すると、SQLにlimit
やoffset
などのデータベースにあわせた件数指定が付与される。
(Oracleではlimit
句やoffset
句が存在しないため、row_num
を指定する必要があるが、そうしたデータベース製品ごとの違いはJPAが吸収してくれる。)
クエリ結果の取得
クエリの実行および結果の取得はgetSingleResult
またはgetResultList
で行う。
getSingleResult
は名前の通り1件のみ結果を取得するが、結果が取得できない場合はNoResultException
が発生し、2件以上取得できた場合は、NonUniqueResultException
が発生する。
getResultList
は複数件取得の結果をList
に詰めて返す。結果が無い場合は0件のリストとなる。
本題から少し外れるが、上記のサンプルコードではJava8から提供された Optional
とラムダ式を使用して、NoResultException
が発生した場合に空のOptional
を生成するようにした。こうすることで、呼び出し側で単純なnull
判定の代わりに、柔軟な記述ができるようにしている。
JPQLの使用方法(2) - 文字列による定義
文字列によるJPQLは、例えば、画面から入力値によってWHERE条件を可変とするような動的なクエリの生成に使用する。
使用する方法は単純で、EntityManager
のcreateQuery
メソッドの引数にJPQLを直接記述する。
em.createQuery("select m from Member m " + "where m.playerNumber > :num order by m.id", Member.class)
先ほどのcreateNamedQuery
と同様に、このcreateQuery
メソッドもQuery
のインスタンスを生成するので、後の操作はNamedQueryの場合と同じである。
この方式では、クエリ実行のたびにJPQLの解析が行われる。このため、静的なJPQLは、NamedQueryで定義するほうがよいだろう。
結果のマッピング
ここまで紹介してきたJPQLでは、
select m from Member m
のように単一のエンティティをselect
句に指定してきた。
単一の値をselect
句に書いた場合は、その値の型がクエリの戻り値となる。
例えば、
select m.name from Member m
とした場合は、Member#name
の型であるString
となる。
複数の値をselect
句に書いた場合、結果はObject
配列となる。
そのため、以下のような煩わしいコードを書く必要がある。
Object[] result = em.createQuery( "select m,t from Member m join Team t " + "on m.belongs = t where m.id = 1", Object[].class) .getSingleResult(); // 取得結果のキャストが必要 System.out.println(((Member)result[0]).getName()); System.out.println(((Team)result[1]).getName());
JPQLでは、これに対応するためコンストラクタ式という文法が用意されている。
コンストラクタ式とは、任意のクラスにselect
句の取得結果を設定する機能である。
チーム名と、そのチームに所属するメンバー数のようなサマリを取得する例を使って説明しよう。
まず、サマリに対応するクラスを作成する。ポイントは引数ありのコンストラクタを持たせることだ。
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が続く
そして、JPQLでは、new
のキーワードで上記クラスのコンストラクタを呼び出し、select
句で取得したい値を中に書く。
こうすることで、クエリ結果の型はTeamSummary
クラスとなる。
List<TeamSummary> summary = em.createQuery( "select new sample.dto.TeamSummary(t.name, count(t)) " + "from Member m join Team t on m.belongs = t group by t", TeamSummary.class ).getResultList(); for(TeamSummary s : summary) { System.out.println(s.getTeamName() + ":" + s.getMemberCount()); }
このようにすれば、オブジェクト配列を扱うことなく様々な結果を取得できる。
コンストラクタ式を使えば永続性コンテキストにキャッシュされない
select
句にエンティティを指定した場合、クエリから取得したエンティティは永続性コンテキストの管理対象として扱われる。
これは、クエリから取得したエンティティのプロパティなどを変更すれば、自動的にデータベースと同期されることを意味する。
また、エンティティに関連がある場合は、関連を辿ったデータ取得も自動で行われることも意味する(これにより、 N+1問題といわれるパフォーマンスの問題を引き起こす可能性がある。この問題に関しては、今後の記事で解説する)。
大量データを取得する場合に永続性コンテキストにエンティティがキャッシュされ、メモリを圧迫する問題の対処方法については、過去の記事でも紹介したが、上で解説したコンストラクタ式を用いる方法でも回避できる。
サンプルで示したとおり、コンストラクタ式に使用するクラスはエンティティである必要は無く、エンティティでないクラスは永続化コンテキストにキャッシュされないためだ。(ただし、プロパティとしてエンティティを持つ場合、そのエンティティについては永続化コンテキストにキャッシュされる)。
サマリの取得のような、取得したい情報だけを定義したクラスをコンストラクタ式に指定することで、永続性コンテキストにエンティティをキャッシュすることなく、必要なデータを取得できる。
更新・削除クエリ
Query
を使って、UPDATE
文やDELETE
文を発行することも可能だ。エンティティのプロパティの更新やEntityManger#remove
による削除は、1件ずつSQLが実行されるが、この方法を使えば、複数件のレコードをまとめて操作できる。
次のように、更新・削除のクエリを作成し、executeUpdate
メソッドによってSQLを実行すればよい。
int deleteted = em.createQuery( "Delete from Member m where m.name like :name") .setParameter("name", "P%") .executeUpdate();
固有な関数の実行
JPQLで使用可能な関数は、ここに書かれている。
ただし、JPA2.1 (Java EE7)から、JPQLにfunction
句が追加され、ユーザー定義関数など任意の関数を利用できるようになった。以前はこのような場合、次回解説するNative Queryを使用するしかなかったため、Native Queryを減らすことができるだろう。
2つの引数を取るmyFunc
関数がある場合、以下のようにfunction
句に、関数名と引数を指定して実行する。
select function('myFunc', e.name, 100) from Member m
まとめ
今回はJPQLの使い方とクエリの実行方法について解説した。 次回はNative Queryについて解説する。
[前多 賢太郎]