JPA de données de printemps: créer des jointures Fetch de requête de spécification

TL;DR : comment répliquer les opérations de jointure-Fetch JPQL en utilisant les spécifications dans les données de printemps JPA?

j'essaie de construire une classe qui va gérer la construction de requête dynamique pour les entités JPA en utilisant des données de printemps JPA. Pour ce faire, je suis en train de définir un certain nombre de méthodes qui créent des objets Predicate (comme c'est suggéré dans le Spring Data JPA docs et ailleurs), et ensuite de les enchaîner quand le paramètre de requête est présentée. Certaines de mes entités ont des relations individuelles avec d'autres entités qui les aident à les décrire, qui sont avidement récupérées lorsqu'elles sont interrogées et fusionnées en collections ou cartes pour la création de 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 la chaîne de requête sont passés de la classe Controller à la classe Service en tant que paires de valeurs clés, où ils sont traités et assemblés en 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 aussi ses enregistrements associés GeneAttribute et GeneSymbolAlias . Cela fonctionne comme prévu, et une demande pour un seul Gene déclenchera 3 requêtes: une pour chacune des tables Gene , GeneAttribute , et GeneSymbolAlias .

le problème est qu'il n'y a aucune raison que 3 requêtes doivent s'Exécuter pour obtenir une seule entité Gene avec des attributs et des alias intégrés. Ce peut être fait en SQL simple, et il peut être fait avec une requête JPQL dans mon dépôt JPA de données de printemps:

@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 en utilisant des spécifications? J'ai trouvé cette question ici , mais il semble seulement faire des fentes paresseuses en fentes avides.

21
demandé sur Community 2015-03-30 17:22:37

3 réponses

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);
    }
}

Utilisation:

    List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol"));
18
répondu DonCziken 2015-07-06 17:19:14

vous pouvez spécifier le fetch de jointure tout en créant la spécification, mais puisque la même spécification sera utilisée par des méthodes pageables aussi comme findAll (Spécification var1, Pageable var2) et count query se plaindra à cause de join fetch. Par conséquent, pour gérer que nous pouvons vérifier le type de résultat de CriteriaQuery et appliquer la jointure que si elle n'est pas Longue (type de résultat pour la requête de Compte). voir code ci-dessous:

    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);
    };
}
4
répondu suraj bahl 2016-11-24 19:13:59

je suggère cette bibliothèque pour spécification. https://github.com/tkaczmarzyk/specification-arg-resolver

de cette bibliothèque: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch



Vous pouvez utiliser l'annotation @JoinFetch pour spécifier les chemins pour effectuer la jointure fetch. 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);
}
2
répondu kafkas 2017-12-30 20:24:46