Filtrowanie wierszy bazy danych za pomocą spring-data-jpa i spring-mvc

Mam projekt spring-mvc, który używa spring-data-jpa do dostępu do danych. Mam obiekt domeny o nazwie Travel, który chcę umożliwić użytkownikowi końcowemu zastosowanie do niego wielu filtrów.

W tym celu zaimplementowałem następujący kontroler:]}
@Autowired
private TravelRepository travelRep;

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

To działa dobrze: użytkownik ma formularz z pole wejściowe lastName, które można wykorzystać do filtrowania podróży.

Poza lastName, mój obiekt Travel domain ma o wiele więcej atrybutów, według których chciałbym filtrować. Myślę, że że gdyby te atrybuty były wszystkimi łańcuchami, to mógłbym dodać je jako @RequestParam s i dodać metodę spring-data-jpa do zapytań za ich pomocą. Na przykład dodałbym metodę findByLastNameLikeAndFirstNameLikeAndShipNameLike.

Jednak nie wiem, jak mam to zrobić, gdy muszę filtrować dla obcych kluczy. Więc mój Travel ma atrybut period, który jest kluczem obcym do obiektu Period domain, który muszę mieć jako rozwijaną listę dla użytkownika, aby wybrać Period.

To, co chcę zrobić, to gdy okres jest null chcę odzyskać wszystkie Podróże filtrowane przez lastName i gdy okres nie jest null Chcę pobrać wszystkie podróże dla tego okresu filtrowane przez lastName.

Wiem, że można to zrobić, jeśli zaimplementuję dwie metody w moim repozytorium i użyjęif do mojego kontrolera:

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

Czy Jest jakiś sposób, aby to zrobić bez używając if ? Moja Podróż ma nie tylko okres, ale także inne atrybuty, które muszą być filtrowane za pomocą listy rozwijanej !! Jak można zrozumieć, złożoność byłaby wykładniczo wzrosła, gdy muszę użyć więcej rozwijanych, ponieważ wszystkie kombinacje muszą być brane pod uwagę: ({]}

Update 03 / 12 / 13 : kontynuując z doskonałej odpowiedzi M. Deinum, a po jej faktycznie wdrożeniu, chciałbym podać kilka uwag dla kompletności pytania/asnwer: {]}

  1. Zamiast implementacji JpaSpecificationExecutor powinieneś zaimplementować JpaSpecificationExecutor<Travel>, aby uniknąć ostrzeżeń typu check.

  2. Proszę spojrzeć na doskonałą odpowiedź kostja na to pytanie Really dynamic JPA CriteriaBuilder ponieważ będzie trzeba zaimplementować to, jeśli chcesz mieć poprawne filtry.

  3. Najlepszą dokumentacją, jaką udało mi się znaleźć dla API Criteria była http://www.ibm.com/developerworks/library/j-typesafejpa/. jest to dość długa lektura, ale zdecydowanie polecam - po przeczytaniu odpowiedzi na większość moich pytań do roota i Criteriabuildera :)

  4. Reusing the Travel obiekt nie był możliwy, ponieważ zawierał różne inne obiekty (które również zawierały inne obiekty), które musiałem wyszukać za pomocą Like - zamiast tego użyłem obiektu TravelSearch, który zawierał pola, których potrzebowałem szukać.

Aktualizacja 10/05/15: zgodnie z prośbą @ priyank, oto jak zaimplementowałem obiekt TravelSearch: {]}

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

Ten obiekt był używany przez TravelSpecification (większość kodu jest specyficzna dla domeny, ale zostawiam go tam jako przykład):

public class TravelSpecification implements Specification<Travel> {
    private TravelSearch criteria;


    public TravelSpecification(TravelSearch ts) {
        criteria= ts;
    }

    @Override
    public Predicate toPredicate(Root<Travel> root, CriteriaQuery<?> query, 
            CriteriaBuilder cb) {
        Join<Travel, Candidacy> o = root.join(Travel_.candidacy);

        Path<Candidacy> candidacy = root.get(Travel_.candidacy);
        Path<Student> student = candidacy.get(Candidacy_.student);
        Path<String> lastName = student.get(Student_.lastName);
        Path<School> school = student.get(Student_.school);

        Path<Period> period = candidacy.get(Candidacy_.period);
        Path<TravelStatusEnum> travelStatus = root.get(Travel_.travelStatus);
        Path<TravelTypeEnum> travelType = root.get(Travel_.travelType);

        Path<Company> company = root.get(Travel_.company);
        Path<String> companyName = company.get(Company_.name);

        final List<Predicate> predicates = new ArrayList<Predicate>();
        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()]));

    }
}

Wreszcie, oto moja metoda wyszukiwania:

@RequestMapping("/search")  
public ModelAndView search(
        @ModelAttribute TravelSearch travelSearch,
        Pageable pageable) {  
    ModelAndView mav = new ModelAndView("travels/list");  

    TravelSpecification tspec = new TravelSpecification(travelSearch);

    Page<Travel> travels  = travelRep.findAll(tspec, pageable);

    PageWrapper<Travel> page = new PageWrapper<Travel>(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;
}
Mam nadzieję, że pomogłem!
Author: Community, 2013-11-29

1 answers

Na początek powinieneś przestać używać @RequestParam i umieścić wszystkie pola wyszukiwania w obiekcie (może do tego celu użyć ponownie obiektu podróży). Następnie masz 2 opcje, których możesz użyć do dynamicznego budowania zapytania

  1. użyj JpaSpecificationExecutor i napisz Specification
  2. użyj QueryDslPredicateExecutor i użyj QueryDSL do napisania predykatu.

Za pomocą JpaSpecificationExecutor

Najpierw dodaj JpaSpecificationExecutor do swojego TravelRepository to da ci metodę findAll(Specification) i możesz usunąć swój własny finder metody.

public interface TravelRepository extends JpaRepository<Travel, Long>, JpaSpecificationExecutor<Travel> {}

Następnie możesz utworzyć metodę w repozytorium, która używa Specification, która zasadniczo buduje zapytanie. Zobacz dokumentację Spring Data JPA .

Jedyne, co musisz zrobić, to utworzyć klasę, która implementuje Specification i która buduje zapytanie na podstawie dostępnych pól. Zapytanie jest budowane przy użyciu JPA criteria API link.

public class TravelSpecification implements Specification<Travel> {

    private final Travel criteria;

    public TravelSpecification(Travel criteria) {
        this.criteria=criteria;
    }

    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        // create query/predicate here.
    }
}

I na koniec trzeba zmodyfikować kontroler, aby używał nowej metody findAll (wziąłem wolności, aby go trochę posprzątać).

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

Za pomocą QueryDslPredicateExecutor

Najpierw dodaj QueryDslPredicateExecutor do swojego TravelRepository to da ci metodę findAll(Predicate) i możesz usunąć własne metody Findera.

public interface TravelRepository extends JpaRepository<Travel, Long>, QueryDslPredicateExecutor<Travel> {}

Następnie zaimplementujesz metodę usługi, która użyje obiektu Travel do zbudowania predykatu przy użyciu QueryDSL.

@Service
@Transactional
public class TravelService {

    private final TravelRepository travels;

    public TravelService(TravelRepository travels) {
        this.travels=travels;
    }

    public Iterable<Travel> search(Travel criteria) {

        BooleanExpression predicate = QTravel.travel...
        return travels.findAll(predicate);
    }
}

Zobacz też ten post.

 49
Author: M. Deinum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-28 11:42:31