読者です 読者をやめる 読者になる 読者になる

DeltaSpike Data Module(使いそうなところだけ)まとめ3

Query Annotations

@QueryでJPQLが使える。

Queryアノテーション使う(Using Query Annotations)
  • Repositoryに書く場合
public interface PersonRepository extends EntityRepository<Person, Long> {
    @Query("select count(p) from Person p where p.age > ?1")
    Long countAllOlderThan(int minAge);
}
  • Entityの@NamedQueriesを呼び出す場合
@Entity
@NamedQueries({
    @NamedQuery(name = Person.BY_MIN_AGE,
                query = "select count(p) from Person p where p.age > ?1 order by p.age asc")
})
public class Person {
    public static final String BY_MIN_AGE = "person.byMinAge";
}

@Repository
public interface PersonRepository extends EntityRepository<Person, Long> {
    @Query(named = Person.BY_MIN_AGE)
    Long countAllOlderThan(int minAge);
}

メソッド引数の順番に、?1, ?2・・・とパラメータがセットされる。パラメータの番号が1始まりなのはPreparedStatementからの伝統(超私見)。
Repositoryを使うのなら、EntityのNamedQueryにクエリを書くよりRepositoryに@Queryで書くほうがいいと思われる。Repositoryにクエリが集約するので。

  • 名前形式パラメータ

JPQLのパラメータを名前形式にする場合はメソッドパラメータに@QueryParamを使う。

public interface PersonRepository extends EntityRepository<Person, Long> {
    @Query("select count(p) from Person p where p.age > :age")
    Long countAllOlderThan(@QueryParam("age") int age);
}

メソッド引数が長くなるけどこっちのほうが分かりやすいか?
@QueryParamを使わないといけない理由はあえて注釈がついている。たぶんコンパイルするとメソッド引数名は保持されないから。引数名がそのまま残るのなら、JPQLのパラメータと引数名を揃えればOK、になるのだろうが。

  • SQL(not JPQL)を使う
@Query(value = "SELECT * FROM PERSON_TABLE p WHERE p.AGE > ?1", isNative = true)
List<Person> findAllOlderThan(int minAge);

isNativeをtrueにするとSQLを書ける。DB固有のクエリとか書くハメになったら。

@Queryのオプション(Annotation Options)
@Query(named = Person.BY_MIN_AGE, max = 10, lock = LockModeType.PESSIMISTIC_WRITE)
List<Person> findAllForUpdate(int minAge);

maxはLIMIT指定、lockはロックモード・・・あれ? OFFSETは? ソース見ても無さそう。。。
いやQueryResult(後述)使えばできるんだけど、OFFSETはつけてよApacheさん。

SQLクエリオプション(Query Options)

SQLクエリのオプションを指定する場合、QueryResultを使う。

@Repository
public interface PersonRepository extends EntityRepository<Person, Long> {
    @Query("select p from Person p where p.age between ?1 and ?2")
    QueryResult<Person> findAllByAge(int minAge, int maxAge);
}

戻り値はListやEntityではなくQueryResultにする。

List<Person> result = personRepository.findAllByAge(18, 65)
    .orderAsc(Person_.lastName)
    .orderDesc(Person_.age)
    .lockMode(LockModeType.WRITE)
    .hint("org.hibernate.timeout", Integer.valueOf(10))
    .getResultList();

ソートやLIMIT、OFFSET、ロックモード等を指定できる。最後にgetResultList()でリスト取得。


・・・というかsortAsc()のPerson_.lastNameって何?
JavaEE使い方メモ(JPA その4 - クライテリアAPI) - Qiita
なるほど。メタモデルというのね。コンパイルでミスを防げるのか、便利。
でもJPQL書いてる時点で防げない部分は出てくるよね。。。


閑話休題
ちなみにString引数メソッドも用意されていて、プロパティ名をStringで指定できる。
あと本家ドキュメントはorderBy系メソッド名が間違えていて、sortAsc(), sortDesc()になっているがorderAsc(), orderDesc()が正しい。

ページング(Pagination)
QueryResult<Person> paged = personRepository.findByAge(age)
    .maxResults(10)
    .firstResult(50);

QueryResult<Person> paged = personRepository.findByAge(age)
    .withPageSize(10)
    .toPage(5);

int totalPages = paged.countPages();

LIMIT, OFFSETを指定するパターンと、ページサイズ, ページを指定するパターンの2つ使えるらしい。お好みで。
ただプロジェクト内で統一しておかないと後々面倒。

複数データ操作(Bulk Operations)
@Repository
public interface PersonRepository extends EntityRepository<Person, Long> {
    @Modifying
    @Query("update Person as p set p.classifier = ?1 where p.classifier = ?2")
    int updateClassifier(Classifier current, Classifier next);
}

1件づつの更新や削除ではなく、まとめて更新処理するクエリには@Modifyingを付ける、ということ?
とりあえず試してみる。

@Query("delete from User as u where u.name = ?1")
public abstract int deleteUsers(String name);

@Modifyingを付けずに実行してみるとエラーが出て怒られた。
「You cannot call getSingleResult() on this query. It is the incorrect query type.」

@Modifyingを付けたらDELETE成功。複数更新/削除クエリには@Modifying必須。

クエリ結果オプション(Optional Query Results)

JPAではgetSingleResult()の結果が0件か複数件になるとException(RuntimeException)になる。 ・・・そうなんだ。
複数件はともかく0件のためにtry-catchはどうなんだ。まあnullチェックするから大差ないと言えなくもない。
DeltaSpkie Dataではオプションで変えれるらしい。

0 or 1件結果(Zero or One Result)

0件の場合はnullを返してくれるオプション。

Person findOptionalBySsn(String ssn);

@Query(named = Person.BY_NAME, singleResult = SingleResultType.OPTIONAL)
Person findByName(String firstName, String lastName);

メソッド式の場合は接頭語に「findOptionalBy」を付ける。
@Queryの場合は「singleResult = SingleResultType.OPTIONAL」を付ける。

複数件ヒットしてしまったらNonUniqueResultException。

なんでも来い(Any Result)

0件の場合はnull、複数件の場合は先頭行を返すオプション。
何て嬉しい、、、いや嬉しいのか?
間違ったクエリがもみ消されることになりかねない。よっぽど意図的に使わないと危険。

Person findAnyByLastName(String lastName);

@Query(named = Person.BY_NAME, singleResult = SingleResultType.ANY)
Person findByName(String firstName, String lastName);

メソッド式の場合は接頭語に「findAnyBy」を付ける。
@Queryの場合は「singleResult = SingleResultType.ANY」を付ける。

Exceptionは発生しない。

※2015/11/30 追記
JPQLやNamedQueryを使用しない場合、つまりメソッド式の場合でもクエリ結果オプションが使えるみたい。

    @Query(singleResult = SingleResultType.OPTIONAL)
    List<Person> findByNameLikeAndAgeBetweenAndGender(String name,
                              int minAge, int maxAge, Gender gender);

ただ不満なのが、@Queryがメソッドにしか指定できない。
1つのメソッドだけオプション指定忘れてNoResultException、とかなる可能性がある。
Repository全体に適用できる(ElementType.TYPE)もあると統一できて楽だけど、まあ、ちゃんとテストしろという話か。