S-JIS[2017-10-07/2017-10-08] 変更履歴

Spring Boot JPQL 結合

Spring BootJPQLのjoinについて。


概要

エンティティーで関連付け(ManyToOne, OneToMany, ElementCollection)を行っていないエンティティー(Entityクラス内でEntityフィールドを定義していない)に対しては、JPQLでjoinを実行できない(と思われる)。

JPQLにもjoinという構文はあるが、普通のSQLと異なり、「Entity内に定義したEntityフィールド」にしか使えない。[/2017-10-08]
JPQLのjoinの例


直積(cross join)

1エンティティーを返す直積

1つのエンティティーを返す直積であれば、JPQLでも記述できる。

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class JoinExampleEntity {

	@Id
	@GeneratedValue
	private Long id;

	private String value;

	private long master1Id; // Master1Entityへの結合キー

	〜setter/getter〜
}
@Entity
public class Master1Entity {

	@Id
	@GeneratedValue
	private Long id;

	private String value;

	〜setter/getter〜
}

src/main/java/com/example/demo/db/repository/JoinExampleRepository.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;

import com.example.demo.db.domain.JoinExampleEntity;
@Repository
public interface JoinExampleRepository extends JpaRepository<JoinExampleEntity, Long> {

	@Query("select t from JoinExampleEntity t, Master1Entity m where m.id = t.master1Id and m.value = :value")
	public List<JoinExampleEntity> findByMaster1Value_crossJoin(@Param("value") String value);
}

JPQL上は、from句でEntity名をカンマ区切りで並べる形になる。
(H2DBに対して生成されるSQLはcross joinになっていた)

この例では、selectで取得するのはJoinExampleEntityだけなので、JavaのクラスとしてはMaster1Entityは出てこない。
そのため、Master1Entityのimportは不要。
(クエリーの文字列の中にMaster1Entityが出てくるが、自動的にEntityクラスと紐付けられる)


1エンティティーを返す直積の動的生成

JpaSpecificationExecutorを使って直積(戻り値はエンティティー1つ)を生成するには、以下の様にする。

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

package com.example.demo.db.repository;

import java.util.List;

import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.example.demo.db.domain.JoinExampleEntity;
import com.example.demo.db.domain.Master1Entity;
@Repository
public interface JoinExampleRepository extends JpaRepository<JoinExampleEntity, Long>, JpaSpecificationExecutor<JoinExampleEntity> {
	public default List<JoinExampleEntity> findByMaster1Value_dynamic(String value) {
		Specification<JoinExampleEntity> spec = (root, query, cb) -> {
			Root<Master1Entity> m = query.from(Master1Entity.class);
			Predicate p1 = cb.equal(m.get("id"), root.get("master1Id"));
			Predicate p2 = cb.equal(m.get("value"), value);
			return cb.and(p1, p2);
		};
		return findAll(spec);
	}
}

複数エンティティーを返す直積

結合された全エンティティーを取得したい場合は、selectの直後にEntityを並べ、メソッドの戻り値をObject配列にする。

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

	@Query("select t, m from JoinExampleEntity t, Master1Entity m where m.id = t.master1Id and m.value = :value")
	public List<Object[]> findByMaster1Value_crossJoin2_1(@Param("value") String value);

呼び出す側:

		List<Object[]> result = repository.findByMaster1Value_crossJoin2_1("m1-2");
		for (Object[] array : result) {
			JoinExampleEntity entity = (JoinExampleEntity) array[0];
			Master1Entity master = (Master1Entity) array[1];
			〜
		}

また、select直後のカラム名(Entity名)に別名(alias)を付けると、Mapで取得することも出来る。

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

	@Query("select t as tx, m as master1 from JoinExampleEntity t, Master1Entity m where m.id = t.master1Id and m.value = :value")
	public List<Map<String, Object>> findByMaster1Value_crossJoin2_2(@Param("value") String value);

呼び出す側:

		List<Map<String, Object>> result = repository.findByMaster1Value_crossJoin2_2("m1-2");
		for (Map<String, Object> map : result) {
			JoinExampleEntity entity = (JoinExampleEntity) map.get("tx");
			Master1Entity master = (Master1Entity) map.get("master1");
			〜
		}

※メソッドの戻り値の型を(配列やMapでなく)JavaBeanにするとConverterNotFoundExceptionが発生する。
 GenericConverterを作ればJavaBeanに変換できそうなのだが、インジェクションできるようになっていないので、無理。


複数エンティティーを返す直積の動的生成

JpaSpecificationExecutorを使う方式ではジェネリクスに返り値のエンティティーを1つだけ指定するので、複数エンティティーを返せない。
EntityManagerを使ってクエリーを生成すれば出来る。

EntityManagerを取得する必要があるので、EntityManagerをフィールドに持ちたい(インジェクションで取得する為)。[/2017-10-11]
しかしRepositoryはインターフェースなので、フィールドを持てない。
こういう場合、RepositoryCustomというインターフェースを用意し、そこでメソッドを宣言した上で、それを実装するクラスを作るという方法がある。

RepositoryCustomのインターフェース名は何でもいいが、慣例としてRepositoryインターフェース名にCustomを付けた名前とするらしい。
一方、これを実装するクラスの名前は、Repositoryインターフェース名にImplを付けた名前でないといけないらしい。
最後に、RepositoryインターフェースはRepositoryCustomインターフェースを継承するようにする。

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

package com.example.demo.db.repository;

import java.util.List;
public interface JoinExampleRepositoryCustom {

	public List<Object[]> findByMaster1Value(String value);
}

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

package com.example.demo.db.repository;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
public class JoinExampleRepositoryImpl implements JoinExampleRepositoryCustom {

	@PersistenceContext
	private EntityManager entityManager;
	@Override
	public List<Object[]> findByMaster1Value(String value) {
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);

		Root<JoinExampleEntity> t = query.from(JoinExampleEntity.class);
		Root<Master1Entity> m = query.from(Master1Entity.class);

		Predicate p1 = cb.equal(m.get("id"), t.get("master1Id"));
		Predicate p2 = cb.equal(m.get("value"), value);
		Predicate where = cb.and(p1, p2);

		query.multiselect(t, m).where(where);

		return entityManager.createQuery(query).getResultList();
	}
}

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

@Repository
public interface JoinExampleRepository extends JoinExampleRepositoryCustom, JpaRepository<JoinExampleEntity, Long> {
〜
}

ネイティブクエリーを使ったjoin

ネイティブクエリー(普通のSQL)を使えば当然joinできる。

1エンティティーだけを返すjoinは以下の様に書ける。

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

@Repository
public interface JoinExampleRepository extends JpaRepository<JoinExampleEntity, Long> {
	@Query(value = "select t.* from join_example_entity t join master1entity m on m.id = t.master1id where m.value = :value", nativeQuery = true)
	public List<JoinExampleEntity> findByMaster1Value(@Param("value") String value);

@QueryアノテーションのnativeQueryをtrueにする。

ネイティブクエリーなので、from句にはEntityクラス名でなくテーブル名を指定する。


結合された全カラムをEntityの形で取得するのは単純には出来なさそう。

selectするカラムのカラム名が重複しているとエラーになるので、全カラムに一意な名前(別名)を付ける必要がある。
すると、「*」で全カラムを指定することが出来ない。

また、返り値の型はObject配列になり、各カラムの値が配列の各要素になる。

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

	@Query(value = "select t.id, t.value, m.id m_id, m.value m_value from join_example_entity t join master1entity m on m.id = t.master1id where m.value = :value", nativeQuery = true)
	public List<Object[]> findByMaster1Value2(@Param("value") String value);

呼び出す側:

		List<Object[]> result = repository.findByMaster1Value2("m1-2");
		for (Object[] array : result) {
			long t_id = ((Number) array[0]).longValue();
			String t_value = (String) array[1];
			long m_id = ((Number) array[2]).longValue();
			String m_value = (String) array[3];
			〜
		}

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