TL; DR : Comment répliquer les opérations JPQL Join-Fetch en utilisant les spécifications de Spring Data JPA?
J'essaie de créer une classe qui gérera la création de requêtes dynamiques pour les entités JPA à l'aide de Spring Data JPA. Pour ce faire, je définis un certain nombre de méthodes qui créent des objets Predicate
(comme cela est suggéré dans les Spring JPA docs et ailleurs), puis les enchaîne lorsque la requête appropriée Le paramètre est soumis. Certaines de mes entités ont des relations un-à-plusieurs avec d'autres entités qui aident à les décrire, qui sont récupérées avec impatience lorsqu'elles sont interrogées et fusionnées dans des collections ou des cartes pour la création DTO. Un exemple simplifié:
@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...
}
Les paramètres de chaîne de requête sont passés de la classe Controller
à la classe Service
sous forme de paires clé-valeur, où ils sont traités et assemblés dans 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...
}
Dans cet exemple, chaque fois que je veux récupérer un enregistrement Gene
, je veux également ses enregistrements GeneAttribute
et GeneSymbolAlias
associés. Tout cela fonctionne comme prévu et une demande pour un seul Gene
déclenchera 3 requêtes: une pour les tables Gene
, GeneAttribute
et GeneSymbolAlias
.
Le problème est qu'il n'y a aucune raison que 3 requêtes doivent être exécutées pour obtenir une seule entité Gene
avec des attributs et des alias incorporés. Cela peut être fait en SQL simple, et cela peut être fait avec une requête JPQL dans mon référentiel Spring Data JPA:
@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);
Comment puis-je répliquer cette stratégie d'extraction à l'aide des spécifications? J'ai trouvé cette question ici , mais il ne semble que faire des recherches paresseuses des recherches passionnées.
Classe de spécification:
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);
}
}
Usage:
List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol"));
Vous pouvez spécifier l'extraction de jointure lors de la création de la spécification, mais puisque la même spécification sera utilisée par des méthodes paginables comme findAll (spécification var1, Pageable var2) et la requête de comptage se plaindra en raison de l'extraction de jointure. Par conséquent, pour gérer cela, nous pouvons vérifier le resultType de CriteriaQuery et appliquer la jointure uniquement si elle n'est pas longue (type de résultat pour la requête de comptage). voir ci-dessous le code:
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);
};
}
Je suggère cette bibliothèque pour la spécification. https://github.com/tkaczmarzyk/specification-arg-resolver
Depuis cette bibliothèque: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch
Vous pouvez utiliser l'annotation @JoinFetch pour spécifier les chemins sur lesquels effectuer la jointure d'extraction. Par exemple:
@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);
}