S-JIS[2017-10-01/2017-10-11] 変更履歴

Spring Boot JPQL

Spring BootJPAのJPQLについて。


概要

JPAでは、JPQL(Java Persistence Query Language)を使ってクエリーを書くことが出来る。

JPQL自体はJavaEEで定められているもので、Spring Boot独自の機能というわけではないらしい。
SQLに似ているが、微妙に異なる。


Repositoryに記述する例

Repositoryクラス(インターフェース)にJPQLを使ってクエリーを書くことが出来る。

src/main/java/com/example/demo/db/repository/JpaExampleRepository.java:

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内では「:名前」でパラメーターを使用できる。


更新用JPQL

delete等のデータ更新用の文もJPQLで書くことができ、その場合は@Modifyingアノテーションを付ける。

src/main/java/com/example/demo/db/repository/JpaExampleRepository.java:

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でも可。


in句

JPQLでin句を記述する場合、変数を丸括弧で囲む必要は無い。[2017-10-02]
(Spring Boot 1.5.6なら丸括弧で囲んでも大丈夫)

src/main/java/com/example/demo/db/repository/JpaExampleRepository.java:

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件となる(空のリストが返ってくる)。


複数カラムのin句

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')」と同じなので、動的に条件を生成すれば、実現できなくはない。

src/main/java/com/example/demo/db/repository/JpaExampleRepository.java:

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);

動的なWHERE条件

JPQLで動的にWHERE条件を作成するには、criteria API(JpaSpecificationExecutor・Specification)を使用する。[2017-10-02]

参考: tag1216さんのSpring Data JPA の Specificationでらくらく動的クエリー


まず、Repositoryインターフェースの親インターフェースにJpaSpecificationExecutorを追加する。

src/main/java/com/example/demo/db/repository/JpaExampleRepository.java:

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インスタンスを作成する)。それをメソッドの引数に渡すことで、その条件で検索できる。


呼び出す側は以下のようになる。

src/main/java/com/example/demo/db/repository/JpaExampleService.java:

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を使っている)

src/main/java/com/example/demo/db/repository/JpaExampleRepository.java:

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);
	}

RepositoryCustom

もしフィールドに何か保持したい(例えばEntityManagerをインジェクションによって取得したいとかの)場合は、
RepositoryCustomインターフェースを用意し(ここでメソッドを宣言する)、それをRepositoryインターフェースで継承した上で、RepositoryImplクラスで実装する
という方法がある。[2017-10-11]

RepositoryCustom・RepositoryImplの例


JPAへ戻る / Spring Bootへ戻る / Spring Frameworkへ戻る / 技術メモへ戻る
メールの送信先:ひしだま