TL; DR:Spring Data JPAの仕様を使用してJPQL Join-Fetch操作をどのように複製しますか?
Spring Data JPAを使用して、JPAエンティティの動的クエリ構築を処理するクラスを構築しようとしています。これを行うために、Predicate
オブジェクト( Spring Data JPA docs などで推奨されている)を作成するいくつかのメソッドを定義し、適切なクエリ時にそれらをチェーンしますパラメータが送信されます。私のエンティティの中には、他のエンティティと1対多の関係を持っているものがあります。これらのエンティティは、DTO作成用のコレクションまたはマップに照会および合体されると、それらを簡単に取得します。簡単な例:
@Entity
public class Gene {
@Id
@Column(name="entrez_gene_id")
privateLong id;
@Column(name="gene_symbol")
private String symbol;
@Column(name="species")
private String species;
@OneToMany(mappedBy="gene", fetch=FetchType.EAGER)
private Set<GeneSymbolAlias> aliases;
@OneToMany(mappedBy="gene", fetch=FetchType.EAGER)
private Set<GeneAttributes> attributes;
// etc...
}
@Entity
public class GeneSymbolAlias {
@Id
@Column(name = "alias_id")
private Long id;
@Column(name="gene_symbol")
private String symbol;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="entrez_gene_id")
private Gene gene;
// etc...
}
クエリ文字列パラメーターはController
クラスからService
クラスにキーと値のペアとして渡され、そこで処理されてPredicates
に組み立てられます。
@Service
public class GeneService {
@Autowired private GeneRepository repository;
@Autowired private GeneSpecificationBuilder builder;
public List<Gene> findGenes(Map<String,Object> params){
return repository.findAll(builder.getSpecifications(params));
}
//etc...
}
@Component
public class GeneSpecificationBuilder {
public Specifications<Gene> getSpecifications(Map<String,Object> params){
Specifications<Gene> = null;
for (Map.Entry param: params.entrySet()){
Specification<Gene> specification = null;
if (param.getKey().equals("symbol")){
specification = symbolEquals((String) param.getValue());
} else if (param.getKey().equals("species")){
specification = speciesEquals((String) param.getValue());
} //etc
if (specification != null){
if (specifications == null){
specifications = Specifications.where(specification);
} else {
specifications.and(specification);
}
}
}
return specifications;
}
private Specification<Gene> symbolEquals(String symbol){
return new Specification<Gene>(){
@Override public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder builder){
return builder.equal(root.get("symbol"), symbol);
}
};
}
// etc...
}
この例では、Gene
レコードを取得するたびに、関連するGeneAttribute
およびGeneSymbolAlias
レコードも必要です。これはすべて期待どおりに機能し、単一のGene
に対するリクエストは3つのクエリを起動します:Gene
、GeneAttribute
、およびGeneSymbolAlias
テーブルに対する各クエリ。
問題は、属性とエイリアスが埋め込まれた単一のGene
エンティティを取得するために3つのクエリを実行する必要がない理由です。これはプレーンSQLで実行でき、Spring Data JPAリポジトリのJPQLクエリを使用して実行できます。
@Query(value = "select g from Gene g left join fetch g.attributes join fetch g.aliases where g.symbol = ?1 order by g.entrezGeneId")
List<Gene> findBySymbol(String symbol);
仕様を使用してこのフェッチ戦略を複製するにはどうすればよいですか? この質問はこちら を見つけましたが、遅延フェッチを熱心なフェッチにしかしないようです。
仕様クラス:
public class MatchAllWithSymbol extends Specification<Gene> {
private String symbol;
public CustomSpec (String symbol) {
this.symbol = symbol;
}
@Override
public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//This part allow to use this specification in pageable queries
//but you must be aware that the results will be paged in
//application memory!
Class clazz = query.getResultType();
if (clazz.equals(Long.class) || clazz.equals(long.class))
return null;
//building the desired query
root.fetch("aliases", JoinType.LEFT);
root.fetch("attributes", JoinType.LEFT);
query.distinct(true);
query.orderBy(cb.asc(root.get("entrezGeneId")));
return cb.equal(root.get("symbol"), symbol);
}
}
使用法:
List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol"));
仕様の作成中に結合フェッチを指定できますが、同じ仕様がfindAll(Specification var1、Pageable var2)などのページング可能なメソッドによって使用されるため、カウントクエリは結合フェッチのために文句を言います。したがって、それを処理するために、CriteriaQueryのresultTypeを確認し、Long(カウントクエリの結果タイプ)でない場合にのみ結合を適用できます。以下のコードを参照してください:
public static Specification<Item> findByCustomer(Customer customer) {
return (root, criteriaQuery, criteriaBuilder) -> {
/*
Join fetch should be applied only for query to fetch the "data", not for "count" query to do pagination.
Handled this by checking the criteriaQuery.getResultType(), if it's long that means query is
for count so not appending join fetch else append it.
*/
if (Long.class != criteriaQuery.getResultType()) {
root.fetch(Person_.itemInfo.getName(), JoinType.LEFT);
}
return criteriaBuilder.equal(root.get(Person_.customer), customer);
};
}
このライブラリを仕様に提案します。 https://github.com/tkaczmarzyk/specification-arg-resolver
このライブラリから: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch
@ JoinFetchアノテーションを使用して、フェッチ結合を実行するパスを指定できます。例えば:
@RequestMapping("/customers")
public Object findByOrderedOrFavouriteItem(
@Joins({
@Join(path = "orders", alias = "o")
@Join(path = "favourites", alias = "f")
})
@Or({
@Spec(path="o.itemName", params="item", spec=Like.class),
@Spec(path="f.itemName", params="item", spec=Like.class)}) Specification<Customer> customersByItem) {
return customerRepo.findAll(customersByItem);
}