it-swarm.dev

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

Mam projekt Spring-mvc, który używa dostępu do danych Spring-Data-JPA. Mam obiekt domeny o nazwie Travel, który chcę pozwolić użytkownikowi końcowemu na 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;
}

Działa to dobrze: użytkownik ma formularz z polem wprowadzania lastName, którego można użyć do filtrowania podróży.

Poza LastName mój obiekt domeny Travel ma znacznie więcej atrybutów, według których chciałbym filtrować. Myślę, że gdyby wszystkie te atrybuty były ciągami, mógłbym dodać je jako @RequestParams i dodaj metodę spring-data-jpa do zapytania przez te. Na przykład dodałbym metodę findByLastNameLikeAndFirstNameLikeAndShipNameLike.

Nie wiem jednak, jak mam to zrobić, gdy muszę filtrować klucze obce. Więc mój Travel ma atrybut period, który jest kluczem obcym do obiektu domeny Period, który muszę mieć jako listę rozwijaną, aby użytkownik mógł wybrać Period.

Chcę, aby gdy okres był zerowy, chcę odzyskać wszystkie podróże filtrowane według lastName, a gdy okres nie jest zerowy, chcę pobrać wszystkie podróże dla tego okresu filtrowane według lastName.

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

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 istnieje sposób, aby to zrobić bez przy użyciu if? Moja podróż ma nie tylko okres, ale także inne atrybuty, które należy filtrować za pomocą menu rozwijanych !! Jak możesz zrozumieć, złożoność byłaby wykładniczo zwiększona, gdy będę potrzebować więcej rozwijanych menu, ponieważ wszystkie kombinacje należy wziąć pod uwagę :(

Aktualizacja 03.12.2013: Kontynuując doskonałą odpowiedź M. Deinuma i po jej wdrożeniu chciałbym przedstawić kilka uwag dotyczących kompletności pytania/odpowiedzi:

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

  2. Proszę spojrzeć na doskonałą odpowiedź kostji na to pytanie Naprawdę dynamiczny JPA CriteriaBuilder ponieważ ty będziesz musiał zaimplementować to, jeśli chcesz mieć prawidłowe filtry.

  3. Najlepsza dokumentacja, jaką udało mi się znaleźć dla interfejsu API Criteria, to http://www.ibm.com/developerworks/library/j-typesafejpa/ . To raczej długa lektura, ale całkowicie ją polecam - po przeczytaniu na większość moich pytań dotyczących Root i CriteriaBuilder otrzymałem odpowiedź :)

  4. Ponowne użycie obiektu Travel nie było możliwe, ponieważ zawierał on 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óre zawierały pola, których potrzebowałem szukać.

Zaktualizuj 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!

44
Serafeim

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

  1. Użyj JpaSpecificationExecutor i napisz Specification
  2. Użyj QueryDslPredicateExecutor i użyj QueryDSL , aby napisać predykat.

Używanie JpaSpecificationExecutor

Najpierw dodaj JpaSpecificationExecutor do swojej TravelRepository, aby uzyskać metodę findAll(Specification) i możesz usunąć niestandardowe metody Findera.

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

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

Jedyne, co musisz zrobić, to stworzyć klasę, która implementuje Specification i która zbuduje zapytanie w oparciu o dostępne pola. Zapytanie jest budowane przy użyciu łącza API kryteriów JPA.

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 musisz zmodyfikować kontroler, aby używał nowej metody findAll (pozwoliłem sobie 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";
}

Używanie QueryDslPredicateExecutor

Najpierw dodaj QueryDslPredicateExecutor do swojej TravelRepository, aby uzyskać metodę findAll(Predicate) i możesz usunąć niestandardowe metody Findera.

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

Następnie zaimplementujesz metodę usługi, która użyłaby 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 także ten post na mokradle .

62
M. Deinum