Spring BootのJPQLのjoinについて。
|
|
|
エンティティーで関連付け(ManyToOne, OneToMany, ElementCollection)を行っていないエンティティー(Entityクラス内でEntityフィールドを定義していない)に対しては、JPQLでjoinを実行できない(と思われる)。
JPQLにもjoinという構文はあるが、普通のSQLと異なり、「Entity内に定義したEntityフィールド」にしか使えない。[/2017-10-08]
→JPQLのjoinの例
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〜 }
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クラスと紐付けられる)
JpaSpecificationExecutorを使って直積(戻り値はエンティティー1つ)を生成するには、以下の様にする。
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配列にする。
@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で取得することも出来る。
@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インターフェースを継承するようにする。
package com.example.demo.db.repository; import java.util.List;
public interface JoinExampleRepositoryCustom { public List<Object[]> findByMaster1Value(String value); }
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(); } }
@Repository public interface JoinExampleRepository extends JoinExampleRepositoryCustom, JpaRepository<JoinExampleEntity, Long> { 〜 }
ネイティブクエリー(普通のSQL)を使えば当然joinできる。
1エンティティーだけを返すjoinは以下の様に書ける。
@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配列になり、各カラムの値が配列の各要素になる。
@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]; 〜 }