Filtraggio delle righe del database con spring-data-jpa e spring-mvc

Ho un progetto spring-mvc che usa spring-data-jpa per l’accesso ai dati. Ho un object di dominio chiamato Travel che voglio consentire all’utente finale di applicare un numero di filtri ad esso.

Per questo, ho implementato il seguente controller:

 @Autowired private TravelRepository travelRep; @RequestMapping("/search") public ModelAndView search( @RequestParam(required= false, defaultValue="") String lastName, Pageable pageable) { ModelAndView mav = new ModelAndView("travels/list"); Page travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable); PageWrapper page = new PageWrapper(travels, "/search"); mav.addObject("page", page); mav.addObject("lastName", lastName); return mav; } 

Funziona bene: l’utente ha un modulo con una casella di input lastName che può essere utilizzata per filtrare i viaggi.

Oltre a lastName, il mio object dominio Travel ha molti più attributi con cui mi piacerebbe filtrare. Penso che se questi attributi fossero tutte stringhe, potrei aggiungerli come @RequestParam e aggiungere un metodo spring-data-jpa per interrogarli. Ad esempio, aggiungerei un metodo findByLastNameLikeAndFirstNameLikeAndShipNameLike .

Tuttavia, non so come dovrei farlo quando devo filtrare le chiavi esterne. Quindi il mio Travel ha un attributo period che è una chiave esterna per l’object dominio Period , che ho bisogno di avere come un dropdown per l’utente per selezionare il Period .

Quello che voglio fare è quando il periodo è nullo. Voglio recuperare tutti i viaggi filtrati da lastName e quando il periodo non è nullo, voglio recuperare tutti i viaggi per questo periodo filtrati dal cognome.

So che questo può essere fatto se implemento due metodi nel mio repository e utilizzo un if per il mio controller:

 public ModelAndView search( @RequestParam(required= false, defaultValue="") String lastName, @RequestParam(required= false, defaultValue=null) Period period, Pageable pageable) { ModelAndView mav = new ModelAndView("travels/list"); Page travels = null; if(period==null) { travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable); } else { travels = travelRep.findByPeriodAndLastNameLike(period,"%"+lastName+"%", pageable); } mav.addObject("page", page); mav.addObject("period", period); mav.addObject("lastName", lastName); return mav; } 

C’è un modo per farlo senza usare l’ if ? Il mio viaggio non ha solo il periodo, ma anche altri attributi che devono essere filtrati utilizzando i menu a discesa !! Come puoi capire, la complessità sarebbe aumentata esponenzialmente quando ho bisogno di usare più menu a discesa perché tutte le combinazioni devono essere considerate 🙁

Aggiornamento 03/12/13 : Continuando dall’eccellente risposta di M. Deinum e dopo averla implementata, vorrei fornire alcuni commenti per la completezza della domanda / asnwer:

  1. Invece di implementare JpaSpecificationExecutor è necessario implementare JpaSpecificationExecutor per evitare avvisi di tipo check.

  2. Per favore, dai un’occhiata all’eccellente risposta di kostja a questa domanda JPA CriteriaBuilder davvero dynamic dal momento che dovrai implementarla se vuoi avere filtri corretti.

  3. La migliore documentazione che sono riuscito a trovare per l’API Criteria era http://www.ibm.com/developerworks/library/j-typesafejpa/ . Questa è una lettura piuttosto lunga, ma la raccomando assolutamente – dopo averla letta la maggior parte delle mie domande per Root e CriteriaBuilder hanno risposto 🙂

  4. Riusare l’object Travel non era ansible perché conteneva vari altri oggetti (che contenevano anche altri oggetti) che dovevo cercare usando Like – invece ho usato un object TravelSearch che conteneva i campi che dovevo cercare.

Aggiornamento 10/05/15 : come da richiesta di @ priyank, ecco come ho implementato l’object TravelSearch:

 public class TravelSearch { private String lastName; private School school; private Period period; private String companyName; private TravelTypeEnum travelType; private TravelStatusEnum travelStatus; // Setters + Getters } 

Questo object è stato utilizzato da TravelSpecification (la maggior parte del codice è specifico del dominio, ma lo lascio lì come esempio):

 public class TravelSpecification implements Specification { private TravelSearch criteria; public TravelSpecification(TravelSearch ts) { criteria= ts; } @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { Join o = root.join(Travel_.candidacy); Path candidacy = root.get(Travel_.candidacy); Path student = candidacy.get(Candidacy_.student); Path lastName = student.get(Student_.lastName); Path school = student.get(Student_.school); Path period = candidacy.get(Candidacy_.period); Path travelStatus = root.get(Travel_.travelStatus); Path travelType = root.get(Travel_.travelType); Path company = root.get(Travel_.company); Path companyName = company.get(Company_.name); final List predicates = new ArrayList(); if(criteria.getSchool()!=null) { predicates.add(cb.equal(school, criteria.getSchool())); } if(criteria.getCompanyName()!=null) { predicates.add(cb.like(companyName, "%"+criteria.getCompanyName()+"%")); } if(criteria.getPeriod()!=null) { predicates.add(cb.equal(period, criteria.getPeriod())); } if(criteria.getTravelStatus()!=null) { predicates.add(cb.equal(travelStatus, criteria.getTravelStatus())); } if(criteria.getTravelType()!=null) { predicates.add(cb.equal(travelType, criteria.getTravelType())); } if(criteria.getLastName()!=null ) { predicates.add(cb.like(lastName, "%"+criteria.getLastName()+"%")); } return cb.and(predicates.toArray(new Predicate[predicates.size()])); } } 

Finalmente, ecco il mio metodo di ricerca:

 @RequestMapping("/search") public ModelAndView search( @ModelAttribute TravelSearch travelSearch, Pageable pageable) { ModelAndView mav = new ModelAndView("travels/list"); TravelSpecification tspec = new TravelSpecification(travelSearch); Page travels = travelRep.findAll(tspec, pageable); PageWrapper page = new PageWrapper(travels, "/search"); mav.addObject(travelSearch); mav.addObject("page", page); mav.addObject("schools", schoolRep.findAll() ); mav.addObject("periods", periodRep.findAll() ); mav.addObject("travelTypes", TravelTypeEnum.values()); mav.addObject("travelStatuses", TravelStatusEnum.values()); return mav; } 

Spero di aver aiutato!

Per i principianti dovresti smettere di usare @RequestParam e inserire tutti i campi di ricerca in un object (forse riutilizzare l’object Travel per quello). Quindi hai 2 opzioni che puoi utilizzare per creare dynamicmente una query

  1. Utilizzare JpaSpecificationExecutor e scrivere una Specification
  2. Utilizzare QueryDslPredicateExecutor e utilizzare QueryDSL per scrivere un predicato.

Utilizzando JpaSpecificationExecutor

Per prima cosa aggiungi JpaSpecificationExecutor al tuo JpaSpecificationExecutor questo ti darà un findAll(Specification) ed è ansible rimuovere i metodi del finder personalizzato.

 public interface TravelRepository extends JpaRepository, JpaSpecificationExecutor {} 

Quindi puoi creare un metodo nel tuo repository che usa una Specification che fondamentalmente costruisce la query. Vedere la documentazione JPA di Spring Data per questo.

L’unica cosa che devi fare è creare una class che implementa la Specification e che costruisce la query in base ai campi disponibili. La query viene creata utilizzando il collegamento API Criteri JPA.

 public class TravelSpecification implements Specification { private final Travel criteria; public TravelSpecification(Travel criteria) { this.criteria=criteria; } public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { // create query/predicate here. } } 

E infine è necessario modificare il controller per utilizzare il nuovo metodo findAll (mi sono preso la libertà di ripulirlo un po ‘).

 @RequestMapping("/search") public String search(@ModelAttribute Travel search, Pageable pageable, Model model) { Specification spec = new TravelSpecification(search); Page travels = travelRep.findAll(spec, pageable); model.addObject("page", new PageWrapper(travels, "/search")); return "travels/list"; } 

Utilizzando QueryDslPredicateExecutor

Per prima cosa aggiungi QueryDslPredicateExecutor al tuo QueryDslPredicateExecutor questo ti darà un findAll(Predicate) ed è ansible rimuovere i metodi del finder personalizzato.

 public interface TravelRepository extends JpaRepository, QueryDslPredicateExecutor {} 

Quindi implementerai un metodo di servizio che utilizzerà l’object Travel per creare un predicato utilizzando QueryDSL.

 @Service @Transactional public class TravelService { private final TravelRepository travels; public TravelService(TravelRepository travels) { this.travels=travels; } public Iterable search(Travel criteria) { BooleanExpression predicate = QTravel.travel... return travels.findAll(predicate); } } 

Vedi anche questo post di palude .