2015-03-30 10 views
18

TL; DR: W jaki sposób można replikować operacje JPD Join-Fetch przy użyciu specyfikacji w Spring Data JPA?Wiosenne dane JPA: tworzenie zapytań dotyczących specyfikacji zapytań

Próbuję zbudować klasę, która będzie obsługiwać dynamiczne tworzenie zapytań dla podmiotów JPA przy użyciu Spring Data JPA. Aby to zrobić, definiuję wiele metod, które tworzą obiekty Predicate (takie jak sugerowane w Spring Data JPA docs i gdzie indziej), a następnie łączą je, gdy zostanie przesłany odpowiedni parametr zapytania. Niektóre z moich jednostek mają relacje jeden-do-wielu z innymi podmiotami, które pomagają je opisać, które są chętnie pobierane, gdy są wypytywane i łączone w kolekcje lub mapy do tworzenia DTO. Uproszczony przykład:

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

} 

parametry ciągu zapytania przechodzą z klasy Controller do klasy Service jako pary klucza wartości, przy czym są one przetwarzane i montuje się 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... 

} 

W tym przykładzie za każdym razem, gdy chcę odzyskać rekord Gene, chcę również jego skojarzone zapisy GeneAttribute i GeneSymbolAlias. Wszystko działa zgodnie z oczekiwaniami, a żądanie pojedynczego użytkownika Gene wywoła 3 zapytania: po jednym dla tabel Gene, GeneAttribute i GeneSymbolAlias.

Problem polega na tym, że nie ma powodu, aby 3 zapytania były uruchamiane w celu uzyskania pojedynczej jednostki Gene z osadzonymi atrybutami i aliasami. Można to zrobić w zwykły SQL, a można to zrobić z kwerendy JPQL w moim repozytorium Wiosna danych 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); 

Jak można powielać tę strategię pobierania przy użyciu specyfikacji? Znalazłem this question here, ale wydaje się, że tylko leniwy pobieram do pożądliwych pobrań.

+0

Czy próbowałeś z 'root.fetch()' wewnątrz 'toPredicate()'? Coś w rodzaju 'root.fetch (" attributes ", JoinType.LEFT)' –

+0

@PredragMaric: To z niecierpliwością pobierze 'atrybuty', ale nadal wymaga dodatkowego zapytania. Chcę, aby wszystkie pobrania były częścią pojedynczego zapytania. – woemler

+0

Tak, ale powinno być wykonane inne pobranie dla 'aliasów:' root.fetch ("aliasy", JoinType.LEFT) ' –

Odpowiedz

16

Specyfikacja Klasa:

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

Zastosowanie:

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

Dobra wskazówka, jak sprawić, by specyfikacja działała z zadowalającymi zapytaniami SDJPA. +1. –

3

Można określić przyłączenia pobierał przy tworzeniu specyfikacji ale od tej samej specyfikacji zostaną wykorzystane metodami stronicowalnej także jak findAll (specyfikacja var1 , Pageable var2) i kwerendę liczenia będą zgłaszać skargi z powodu pobrania sprzężenia. Dlatego, aby obsłużyć to, możemy sprawdzić resultType z CriteriaQuery i zastosować join tylko, jeśli nie jest Long (typ wyniku dla zapytania liczenia). zobacz poniżej kod:

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

Proponuję tę bibliotekę do specyfikacji. https://github.com/tkaczmarzyk/specification-arg-resolver

z tej biblioteki: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch

Można użyć @JoinFetch adnotacji do określenia ścieżki do wykonywania na pobieraniu przyłączyć. Na przykład:

@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); 
} 
Powiązane problemy