2015-03-30 5 views
18

TL; DR: Wie replizieren Sie JPQL-Join-Fetch-Vorgänge mithilfe von Spezifikationen in Spring Data JPA?Spring-Daten-JPA: Erstellen von Abfrageabfragen für die Spezifikationsabfrage

Ich versuche, eine Klasse zu erstellen, die dynamische Abfrage erstellen für JPA-Entitäten mithilfe von Spring Data JPA. Um dies zu tun, ich definiere eine Anzahl von Methoden, die Predicate Objekte (wie in der Spring Data JPA docs und anderswo vorgeschlagen) erstellen und dann verketten, wenn der entsprechende Abfrageparameter übergeben wird. Einige meiner Entitäten haben Eins-zu-Viele-Beziehungen mit anderen Entitäten, die ihnen helfen, sie zu beschreiben, die bei der Abfrage und Zusammenführung zu Sammlungen oder Karten für die DTO-Erstellung eifrig abgerufen werden. Ein vereinfachtes Beispiel:

@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... 

} 

Abfrage-String-Parameter aus der Controller Klasse die Service Klasse übergeben werden als Schlüssel-Wert-Paare, wo sie verarbeitet werden und zusammengebaut in 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... 

} 

In diesem Beispiel jedes Mal, wenn ich einen Gene Datensatz abrufen möchte, möchte ich auch die zugehörigen GeneAttribute und GeneSymbolAlias Datensätze. Dies funktioniert alles wie erwartet, und eine Anfrage für eine einzige Gene wird 3 Abfragen auslösen: jeweils eine zu den, GeneAttribute und GeneSymbolAlias Tabellen.

Das Problem ist, dass es keinen Grund gibt, dass 3 Abfragen ausgeführt werden müssen, um eine einzige Gene Entität mit eingebetteten Attributen und Aliasen zu erhalten. Dies kann in einfachen SQL durchgeführt werden, und es kann mit einer JPQL Abfrage in meinem Spring Data JPA-Repository erfolgen:

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

Wie kann ich diese Abrufstrategie Spezifikationen mit replizieren? Ich habe this question here gefunden, aber es scheint nur faule Fetches in eifrige Fetches zu machen.

+0

Haben Sie versucht mit 'root.fetch()' in 'toPredicate()'? Etwas wie 'root.fetch (" attributes ", JoinType.LEFT)' –

+0

@PredragMaric: Das wird eifrig die 'Attribute' holen, aber es erfordert noch eine zusätzliche Abfrage. Ich möchte, dass alle Abrufe Teil einer einzigen Abfrage sind. – woemler

+0

Ja, aber ein anderer Aufruf für 'Aliase 'sollte es tun:' root.fetch ("aliases", JoinType.LEFT) ' –

Antwort

16

Spezifikation Klasse:

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

Verbrauch:

List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol")); 
+0

Netter Tipp, wie man eine Spezifikation mit SDJPA-Auslagerungs-Abfragen arbeiten lässt. +1. –

3

Sie können die Join angeben holen, während Spezifikation zu schaffen, aber da der gleichen Spezifikation wird von auslagerungsfähigen Methoden auch wie findAll (Angabe var1 verwendet werden , Seitenvariable var2) und count-Abfrage wird sich wegen Join-Abruf beschweren. Daher können wir den resultType von CriteriaQuery überprüfen und Join nur dann anwenden, wenn es nicht Long ist (Ergebnistyp für die Anzahl der Abfragen). siehe unten 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); 
    }; 
} 
1

Ich empfehle diese Bibliothek für die Spezifikation. https://github.com/tkaczmarzyk/specification-arg-resolver

Aus dieser Bibliothek: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch

können Sie @JoinFetch Annotation verwenden Pfade angeben, auf auszuführen Abruf beizutreten. Zum Beispiel:

@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); 
} 
Verwandte Themen