Spring BootのJPAのJPQLについて。
|
|
JPAでは、JPQL(Java Persistence Query Language)を使ってクエリーを書くことが出来る。
JPQL自体はJavaEEで定められているもので、Spring Boot独自の機能というわけではないらしい。
SQLに似ているが、微妙に異なる。
Repositoryクラス(インターフェース)にJPQLを使ってクエリーを書くことが出来る。
import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository;
@Repository public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long> { 〜 @Query("select t from JpaExampleEntity t where value >= :value") public List<JpaExampleEntity> findGeValue(@Param("value") String value); }
Repositoryのメソッドに@Queryアノテーションを付けてJPQLを記述する。
「select t
」の「t
」は、Entityクラス名の別名(from句で付けた名前)。
通常のSQLであれば「t.*
」に相当する。つまり全カラム(Entityの全フィールド)を取得するという意味。
「from JpaExampleEntity t
」はEntityクラス名と(JPQL内で使用する)別名を表す。
(Entityクラスに@Tableアノテーションでテーブル名を指定している場合でも、JPQLではEntityクラス名を指定する)
where句でEntityクラスのプロパティー(フィールド名)を記述する際は「value
」でも「t.value
」でも可。
メソッドの引数に@Paramアノテーションを付けると、JPQL内で使用するパラメーター名になる。
JPQL内では「:名前
」でパラメーターを使用できる。
delete等のデータ更新用の文もJPQLで書くことができ、その場合は@Modifyingアノテーションを付ける。
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param;
@Query("delete from JpaExampleEntity where value = :value") @Modifying public int deleteByValue(@Param("value") String value);
このメソッドの場合、戻り値は更新件数(deleteの場合は削除した件数)になる。
不要であればvoidでも可。
JPQLでin句を記述する場合、変数を丸括弧で囲む必要は無い。[2017-10-02]
(Spring Boot 1.5.6なら丸括弧で囲んでも大丈夫)
import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param;
@Query("select t from JpaExampleEntity t where t.value in :value") public List<JpaExampleEntity> findByValues(@Param("value") List<String> value);
メソッドの引数に空リストやnullを渡した場合は結果は0件となる(空のリストが返ってくる)。
SQLの場合、「(id, value) in ((1, 'aaa'), (2, 'bbb'))
」のように、複数カラムでin句を使うことが出来る(RDBMSがある)。[2017-10-02]
が、JPQLではこのようなin句には対応していない。
理屈上、これは「(id=1 and value='aaa') or (id=2 and value='bbb')
」と同じなので、動的に条件を生成すれば、実現できなくはない。
import java.util.Collections; import java.util.List; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specifications; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.util.Pair; import org.springframework.stereotype.Repository;
@Repository public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long>, JpaSpecificationExecutor<JpaExampleEntity> {
public default List<JpaExampleEntity> findByIdValues(List<Pair<Long, String>> conditionList) { if (conditionList == null || conditionList.isEmpty()) { return Collections.emptyList(); } Specifications<JpaExampleEntity> spec = Specifications.where(null); for (Pair<Long, String> condition : conditionList) { Specification<JpaExampleEntity> s1 = (root, query, cb) -> cb.equal(root.get("id"), condition.getFirst()); Specification<JpaExampleEntity> s2 = (root, query, cb) -> cb.equal(root.get("value"), condition.getSecond()); spec = spec.or(Specifications.where(s1).and(s2)); } return findAll(spec); }
@Autowired private JpaExampleRepository repository;
List<Pair<Long, String>> conditionList = Arrays.asList(Pair.of(1L, "aaa"), Pair.of(2L, "bbb")); List<JpaExampleEntity> result = repository.findByIdValues(conditionList);
JPQLで動的にWHERE条件を作成するには、criteria API(JpaSpecificationExecutor・Specification)を使用する。[2017-10-02]
参考: tag1216さんのSpring Data JPA の Specificationでらくらく動的クエリー
まず、Repositoryインターフェースの親インターフェースにJpaSpecificationExecutorを追加する。
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long>, JpaSpecificationExecutor<JpaExampleEntity> { 〜 }
JpaSpecificationExecutorインターフェースを追加する事で、findAll(Specification)
メソッド等が呼び出せるようになる。
そして、Specificationを使ってWHERE条件を構築する(Specificationインスタンスを作成する)。それをメソッドの引数に渡すことで、その条件で検索できる。
呼び出す側は以下のようになる。
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specifications;
@Autowired private JpaExampleRepository repository;
public List<JpaExampleEntity> findSpec() { // value = 'aaa' Specification<JpaExampleEntity> spec1 = (root, query, cb) -> cb.equal(root.get("value"), "aaa"); // value = 'bbb' Specification<JpaExampleEntity> spec2 = (root, query, cb) -> cb.equal(root.get("value"), "bbb"); return repository.findAll(Specifications.where(spec1).or(spec2)); }
Specificationで個々の条件を定義し、Specificationsのwhere/and/orメソッドでつないでいく事で複合条件を作成できる。
where/and/orメソッドにnullを渡すと、その条件は生成されない。
Specificationインターフェースは関数型インターフェース(の条件を満たしている)なので、ラムダ式で記述することが出来る。
cb
はCriteriaBuilderで、演算子毎に該当メソッドを呼び出す。(例えば「=」の場合はequalメソッド)
root.get(name)
でカラム(Entityクラスのフィールド名)を取得する。
なお、in句の場合はroot.get("value").in(Arrays.asList("aaa", "bbb"))
の様に、CriteriaBuilderを使わずに記述する。
参考: jabarasterさんのJPAのCriteria QueryでIN句を指定する
ところで、個人的には、JPQLに関するメソッドはRepositoryインターフェースに全て集めたい。
Java8ならインターフェースにdefaultメソッドで実装を書いたりstaticメソッドでサブルーチンを作ったりできるので、それを利用してみる。
(Java9ならインターフェースにprivateメソッドを定義できるのでサブルーチンはprivateにしたいところだが、今回はJava8を使っている)
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specifications; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long>, JpaSpecificationExecutor<JpaExampleEntity> {
public default List<JpaExampleEntity> findSpec2(List<Long> idList, List<String> valueList) { Specifications<JpaExampleEntity> spec = Specifications.where(idIn(idList)).and(valueIn(valueList)); return findAll(spec); } // id in (idList) /* private */ static Specification<JpaExampleEntity> idIn(List<Long> idList) { if (idList == null || idList.isEmpty()) { return null; } return (root, query, cb) -> root.get("id").in(idList); } // value in (valueList) /* private */ static Specification<JpaExampleEntity> valueIn(List<String> valueList) { if (valueList == null || valueList.isEmpty()) { return null; } return (root, query, cb) -> root.get("value").in(valueList); } }
ちなみに、この例ではinの実装はどれも同じになるので、ジェネリクスを使えばメソッドをまとめることが出来る。
public default List<JpaExampleEntity> findSpec2(List<Long> idList, List<String> valueList) { Specifications<JpaExampleEntity> spec = Specifications.where(in("id", idList)).and(in("value", valueList)); return findAll(spec); } /* private */ static <T> Specification<JpaExampleEntity> in(String name, List<T> list) { if (list == null || list.isEmpty()) { return null; } return (root, query, cb) -> root.get(name).in(list); }
もしフィールドに何か保持したい(例えばEntityManagerをインジェクションによって取得したいとかの)場合は、
RepositoryCustomインターフェースを用意し(ここでメソッドを宣言する)、それをRepositoryインターフェースで継承した上で、RepositoryImplクラスで実装する
という方法がある。[2017-10-11]
→RepositoryCustom・RepositoryImplの例