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];
〜
}