web-dev-qa-db-fra.com

Filtrage des lignes de base de données avec Spring-Data-JPA et Spring-MVC

J'ai un projet spring-mvc qui utilise spring-data-jpa pour l'accès aux données. J'ai un objet de domaine appelé Travel que je veux autoriser l'utilisateur final à lui appliquer un certain nombre de filtres.

Pour cela, j'ai implémenté le contrôleur suivant:

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

Cela fonctionne très bien: l'utilisateur a un formulaire avec une zone de saisie lastName qui peut être utilisé pour filtrer les voyages.

Au-delà de lastName, mon objet de domaine Travel a beaucoup plus d'attributs par lesquels j'aimerais filtrer. Je pense que si ces attributs étaient tous des chaînes, je pourrais les ajouter comme @RequestParams et ajoutez une méthode spring-data-jpa pour les interroger. Par exemple, j'ajouterais une méthode findByLastNameLikeAndFirstNameLikeAndShipNameLike.

Cependant, je ne sais pas comment le faire lorsque j'ai besoin de filtrer les clés étrangères. Donc, mon Travel a un attribut period qui est une clé étrangère à l'objet de domaine Period, dont j'ai besoin comme liste déroulante pour que l'utilisateur puisse sélectionner le Period.

Ce que je veux faire, c'est quand la période est nulle, je veux récupérer tous les voyages filtrés par le nom et quand la période n'est pas nulle, je veux récupérer tous les voyages pour cette période filtrée par le nom.

Je sais que cela peut être fait si j'implémente deux méthodes dans mon référentiel et utilise un if sur mon contrôleur:

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

Existe-t-il un moyen de faire cela sans en utilisant le if? My Travel a non seulement la période mais aussi d'autres attributs qui doivent être filtrés à l'aide de listes déroulantes !! Comme vous pouvez le comprendre, la complexité serait exponentiellement augmentée lorsque je devrais utiliser plus de listes déroulantes car toutes les combinaisons devraient être prises en compte :(

Mise à jour 03/12/13: Suite à l'excellente réponse de M. Deinum, et après l'avoir effectivement mise en œuvre, je voudrais fournir quelques commentaires pour que la question/réponse soit complète:

  1. Au lieu d'implémenter JpaSpecificationExecutor, vous devez implémenter JpaSpecificationExecutor<Travel> pour éviter les avertissements de vérification de type.

  2. Veuillez jeter un œil à l'excellente réponse de kostja à cette question Really dynamic JPA CriteriaBuilder puisque vous aurez besoin de l'implémenter si vous le souhaitez d'avoir des filtres corrects.

  3. La meilleure documentation que j'ai pu trouver pour l'API Criteria était http://www.ibm.com/developerworks/library/j-typesafejpa/ . C'est une lecture assez longue mais je la recommande totalement - après l'avoir lue, la plupart de mes questions pour Root et CriteriaBuilder ont été répondues :)

  4. La réutilisation de l'objet Travel n'a pas été possible car il contenait divers autres objets (qui contenaient également d'autres objets) que je devais rechercher en utilisant Like - à la place, j'ai utilisé un objet TravelSearch qui contenait les champs que je devais rechercher.

Mise à jour 10/05/15: Conformément à la demande de @ priyank, voici comment j'ai implémenté l'objet TravelSearch:

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

Cet objet a été utilisé par TravelSpecification (la plupart du code est spécifique au domaine mais je le laisse là comme exemple):

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

    }
}

Enfin, voici ma méthode de recherche:

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

J'espère que j'ai aidé!

44
Serafeim

Pour commencer, vous devez cesser d'utiliser @RequestParam Et mettre tous vos champs de recherche dans un objet (peut-être réutiliser l'objet Voyage pour cela). Ensuite, vous avez 2 options que vous pouvez utiliser pour créer dynamiquement une requête

  1. Utilisez le JpaSpecificationExecutor et écrivez un Specification
  2. Utilisez le QueryDslPredicateExecutor et utilisez QueryDSL pour écrire un prédicat.

Utilisation de JpaSpecificationExecutor

Ajoutez d'abord le JpaSpecificationExecutor à votre TravelRepository cela vous donnera une méthode findAll(Specification) et vous pourrez supprimer vos méthodes Finder personnalisées.

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

Ensuite, vous pouvez créer une méthode dans votre référentiel qui utilise un Specification qui construit essentiellement la requête. Voir le Spring Data JPA documentation pour cela.

La seule chose que vous devez faire est de créer une classe qui implémente Specification et qui construit la requête en fonction des champs disponibles. La requête est créée à l'aide du lien API JPA Criteria.

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

Et enfin vous devez modifier votre contrôleur pour utiliser la nouvelle méthode findAll (j'ai pris la liberté de la nettoyer un peu).

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

Utilisation de QueryDslPredicateExecutor

Ajoutez d'abord le QueryDslPredicateExecutor à votre TravelRepository cela vous donnera une méthode findAll(Predicate) et vous pourrez supprimer vos méthodes Finder personnalisées.

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

Vous implémenteriez ensuite une méthode de service qui utiliserait l'objet Travel pour créer un prédicat à l'aide de 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);
    }
}

Voir aussi ce potea .

62
M. Deinum