wake-up-neo.com

Filtern von Datenbankzeilen mit spring-data-jpa und spring-mvc

Ich habe ein Spring-MVC-Projekt, das Spring-Data-JPA für den Datenzugriff verwendet. Ich habe ein Domänenobjekt mit dem Namen Travel, mit dem der Endbenutzer eine Reihe von Filtern anwenden kann.

Dafür habe ich den folgenden Controller implementiert:

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

Dies funktioniert einwandfrei: Der Benutzer hat ein Formular mit einem Eingabefeld lastName, mit dem die Reisen gefiltert werden können.

Über lastName hinaus verfügt mein Domain-Objekt Travel über viele weitere Attribute, nach denen ich filtern möchte. Ich denke, wenn diese Attribute alle Zeichenfolgen wären, könnte ich sie als @RequestParams und fügen Sie eine spring-data-jpa-Methode hinzu, um diese abzufragen. Zum Beispiel würde ich eine Methode findByLastNameLikeAndFirstNameLikeAndShipNameLike hinzufügen.

Ich weiß jedoch nicht, wie ich es tun soll, wenn ich nach Fremdschlüsseln filtern muss. Mein Travel hat also ein period-Attribut, das ein Fremdschlüssel für das Period -Domänenobjekt ist. Ich benötige es als Dropdown-Liste, damit der Benutzer das Period.

Was ich tun möchte, ist, wenn der Zeitraum null ist. Ich möchte alle Reisen abrufen, die nach dem Nachnamen gefiltert wurden. Wenn der Zeitraum nicht null ist, möchte ich alle Reisen für diesen Zeitraum abrufen, die nach dem Nachnamen gefiltert wurden.

Ich weiß, dass dies möglich ist, wenn ich zwei Methoden in meinem Repository implementiere und einen if für meinen Controller verwende:

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

Gibt es eine Möglichkeit, dies zu tun , ohne das if zu verwenden? Meine Reise hat nicht nur den Zeitraum, sondern auch andere Attribute, die mithilfe von Dropdowns gefiltert werden müssen !! Wie Sie verstehen können, würde die Komplexität exponentiell zunehmen, wenn ich mehr Dropdowns verwenden müsste, da alle Kombinationen berücksichtigt werden müssten :(

Update 12.03.13: Ausgehend von der hervorragenden Antwort von M. Deinum und nachdem ich sie tatsächlich umgesetzt habe, möchte ich einige Kommentare zur Vollständigkeit der Frage/Antwort abgeben:

  1. Anstatt JpaSpecificationExecutor zu implementieren, sollten Sie JpaSpecificationExecutor<Travel>, um Warnungen bei der Typprüfung zu vermeiden.

  2. Bitte werfen Sie einen Blick auf Kostjas ausgezeichnete Antwort auf diese Frage Really dynamic JPA CriteriaBuilder , da Sie dies implementieren müssen , wenn Sie dies möchten richtige Filter haben.

  3. Die beste Dokumentation, die ich für die Kriterien-API finden konnte, war http://www.ibm.com/developerworks/library/j-typesafejpa/ . Dies ist eine ziemlich lange Lektüre, aber ich kann es nur empfehlen - nachdem ich es gelesen habe, wurden die meisten meiner Fragen an Root und CriteriaBuilder beantwortet :)

  4. Die Wiederverwendung des Travel -Objekts war nicht möglich, da es verschiedene andere Objekte enthielt (die auch andere Objekte enthielten), nach denen ich mit Like suchen musste - stattdessen habe ich ein TravelSearch -Objekt verwendet das enthielt die Felder, nach denen ich suchen musste.

Update 10/05/15: Auf Anfrage von @ priyank habe ich das TravelSearch-Objekt folgendermaßen implementiert:

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

Dieses Objekt wurde von TravelSpecification verwendet (der größte Teil des Codes ist domänenspezifisch, ich lasse ihn jedoch als Beispiel):

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()]));

    }
}

Schließlich ist hier meine Suchmethode:

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

Hoffe ich habe geholfen!

44
Serafeim

Für den Anfang sollten Sie die Verwendung von @RequestParam Beenden und alle Ihre Suchfelder in ein Objekt einfügen (verwenden Sie das Reiseobjekt möglicherweise dafür erneut). Dann haben Sie 2 Optionen, mit denen Sie dynamisch eine Abfrage erstellen können

  1. Verwenden Sie das JpaSpecificationExecutor und schreiben Sie ein Specification
  2. Verwenden Sie QueryDslPredicateExecutor und QueryDSL , um ein Prädikat zu schreiben.

JpaSpecificationExecutor verwenden

Fügen Sie zuerst das JpaSpecificationExecutor zu Ihrem TravelRepository hinzu. Dadurch erhalten Sie eine findAll(Specification) -Methode, und Sie können Ihre benutzerdefinierten Finder-Methoden entfernen.

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

Anschließend können Sie in Ihrem Repository eine Methode erstellen, die ein Specification verwendet, das im Grunde die Abfrage erstellt. Siehe hierzu das JPA Dokumentation .

Sie müssen lediglich eine Klasse erstellen, die Specification implementiert und die Abfrage anhand der verfügbaren Felder erstellt. Die Abfrage wird mithilfe des JPA-Kriterien-API-Links erstellt.

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

Und schließlich müssen Sie Ihren Controller ändern, um die neue findAll -Methode zu verwenden (ich habe mir erlaubt, ein wenig aufzuräumen).

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

QueryDslPredicateExecutor verwenden

Fügen Sie zuerst das QueryDslPredicateExecutor zu Ihrem TravelRepository hinzu. Dadurch erhalten Sie eine findAll(Predicate) -Methode, und Sie können Ihre benutzerdefinierten Finder-Methoden entfernen.

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

Als Nächstes würden Sie eine Dienstmethode implementieren, die das Objekt Travel verwendet, um mit QueryDSL ein Prädikat zu erstellen.

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

Siehe auch dieser Moorpfosten .

62
M. Deinum