70

I have a spring-mvc project that is using spring-data-jpa for data access. I have a domain object called Travel which I want to allow the end-user to apply a number of filters to it.

For that, I've implemented the following 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<Travel> travels  = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
    PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
    mav.addObject("page", page);
    mav.addObject("lastName", lastName);
    return mav;
}

This works fine: The user has a form with a lastName input box which can be used to filter the Travels.

Beyond lastName, my Travel domain object has a lot more attributes by which I'd like to filter. I think that if these attributes were all strings then I could add them as @RequestParams and add a spring-data-jpa method to query by these. For instance I'd add a method findByLastNameLikeAndFirstNameLikeAndShipNameLike.

However, I don't know how should I do it when I need to filter for foreign keys. So my Travel has a period attribute that is a foreign key to the Period domain object, which I need to have it as a dropdown for the user to select the Period.

What I want to do is when the period is null I want to retrieve all travels filtered by the lastName and when the period is not null I want to retrieve all travels for this period filtered by the lastName.

I know that this can be done if I implement two methods in my repository and use an if to my 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;
}

Is there a way to do this without using the if ? My Travel has not only the period but also other attributes that need to be filtered using dropdowns !! As you can understand, the complexity would be exponentially increased when I need to use more dropdowns because all the combinations'd need to be considered :(

Update 03/12/13: Continuing from M. Deinum's excelent answer, and after actually implementing it, I'd like to provide some comments for completeness of the question/asnwer:

  1. Instead of implementing JpaSpecificationExecutor you should implement JpaSpecificationExecutor<Travel> to avoid type check warnings.

  2. Please take a look at kostja's excellent answer to this question Really dynamic JPA CriteriaBuilder since you will need to implement this if you want to have correct filters.

  3. The best documentation I was able to find for the Criteria API was http://www.ibm.com/developerworks/library/j-typesafejpa/. This is a rather long read but I totally recommend it - after reading it most of my questions for Root and CriteriaBuilder were answered :)

  4. Reusing the Travel object was not possible because it contained various other objects (who also contained other objects) which I needed to search for using Like - instead I used a TravelSearch object that contained the fields I needed to search for.

Update 10/05/15: As per @priyank's request, here's how I implemented the TravelSearch object:

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

This object was used by TravelSpecification (most of the code is domain specific but I'm leaving it there as an example):

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

    }
}

Finally, here's my search method:

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

Hope I helped!

Community
  • 1
  • 1
Serafeim
  • 14,962
  • 14
  • 91
  • 133
  • How did you convert attributes in TravelSearch to List. Can you share your code ? Thanks. – priyank May 10 '15 at 05:58
  • Sorry what are the "Travel_", "Candidacy_" and other stuff with underscore? My IntelliJ IDEA has no suggestions for that – Andrey M. Stepanov Jan 23 '19 at 15:53
  • 1
    The `Travel_`, `Candidact_` etc objects are metamodels that are created automatiaclly by hibernate and help you build queries. More info : https://docs.jboss.org/hibernate/jpamodelgen/1.0/reference/en-US/html_single/#whatisit – Serafeim Jan 23 '19 at 16:28
  • Sorry again. I did not get if I can compose metamodel myself or for some reason it is supposed to be generated by annotation processor after adding some hibernate specific dependency? At what point metamodel will be generated - during the project build? But how will my code compile if I reference metamodel in my code not yet having class for it? – Andrey M. Stepanov Jan 23 '19 at 22:04
  • 1
    You are correct in that these metamodels are automatically generated. You need to add the hibernate-jpamodelgen dependency to your pom.xml as described here: http://hibernate.org/orm/tooling/. I have many years since I last used that (and I was using eclipse back then) but IIRC when you add the dependency the metamodel classes will be automatically generated by themselves. Then you'll be able to use these metamodels as a dependency to your queries. – Serafeim Jan 23 '19 at 22:08
  • for some reason when I try to compile the project, the compiler is offended with the message like ' error reading C:\Users\user\.m2\repository\org\hibernate\hibernate-jpamodelgen\1.0.1\hibernate-jpamodelgen-1.0.1.jar; error in opening zip file' so metamodel is not generated :( – Andrey M. Stepanov Jan 23 '19 at 22:20
  • ... but at least it seems to be ok to write metamodel by hand – Andrey M. Stepanov Jan 23 '19 at 22:34
  • May be another silly question but should the metamodel class name for some reason end specifically with underscore? We have checkstyle that prohibits this name pattern :) – Andrey M. Stepanov Jan 23 '19 at 23:00
  • And why do we need metamodel in the first place? It looks like a little mess overally: DO + Search Object + Metadata Object. Just imho – Andrey M. Stepanov Jan 24 '19 at 00:12
  • 1
    I am not sure why the metamodel class is not genetated, however adding it yourself is not a good idea since that beats the whole purpose of the metamodel class! The name of the metamodel class probably can be changed to something else dor example TravelMETA but I don't know how! Finally, the metamodel is needed to have static checks to your queries, ie instead of writing hsql which may lead to errors at runtime you'll have checks at compile time. – Serafeim Jan 24 '19 at 08:02
  • @Serafeim. First, I really appreciate a lot your answering my questions. Will you please advise me also: if I implement REST API whether all that need to be changed in the code is to replace '@ModelAttribute TravelSearch travelSearch' with '@RequestParam TravelSearch travelSearch' ... I'm wondering now also how to combine my search form fields so that together they were considered as sort of 'Search' object. Thank you in advance – Andrey M. Stepanov Jan 24 '19 at 21:51
  • Hello @andrey unfortunately I don't use spring anymore (have moved to django) so I can't really answer these implementation specific questions. Best regards, Serafeim – Serafeim Jan 25 '19 at 02:30
  • @Serafeim, thank you so much for being patient ... I solved the problem. May I ask you - do you find Django more comfortable to work with than Spring? – Andrey M. Stepanov Jan 27 '19 at 14:33
  • 1
    You are welcome @AndreyM.Stepanov I'm glad you solved your problem. As for your other question, yes I find Django much more comfortable and straightforward than Spring. There's much less magic (and also the docs are great and the source code easily accessible) enabling you to always know what's wrong! Also, there's a huge amount of battery-included stuff (templates, auth, permissions, admin, forms, tables, the ORM) that "just work"; you could also have these with Spring but it's a painful and difficult journey to properly integrate all these with your app. – Serafeim Jan 27 '19 at 16:20
  • You might consider https://github.com/mhewedy/spring-data-jpa-mongodb-expressions for rest services – Muhammad Hewedy Jan 16 '22 at 21:49

1 Answers1

88

For starters you should stop using @RequestParam and put all your search fields in an object (maybe reuse the Travel object for that). Then you have 2 options which you could use to dynamically build a query

  1. Use the JpaSpecificationExecutor and write a Specification
  2. Use the QueryDslPredicateExecutor and use QueryDSL to write a predicate.

Using JpaSpecificationExecutor

First add the JpaSpecificationExecutor to your TravelRepository this will give you a findAll(Specification) method and you can remove your custom finder methods.

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

Then you can create a method in your repository which uses a Specification which basically builds the query. See the Spring Data JPA documentation for this.

The only thing you need to do is create a class which implements Specification and which builds the query based on the fields which are available. The query is build using the 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.
    }
}

And finally you need to modify your controller to use the new findAll method (I took the liberty to clean it up a little).

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

Using QueryDslPredicateExecutor

First add the QueryDslPredicateExecutor to your TravelRepository this will give you a findAll(Predicate) method and you can remove your custom finder methods.

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

Next you would implement a service method which would use the Travel object to build a predicate using 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);
    }
}

See also this bog post.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • Another question: Do you know where should I find some good info (or the API) on how to implement the "toPredicate" method of TravelSpecification ? Thanks ! – Serafeim Nov 29 '13 at 10:30
  • The JPA documentation and assuming you use hibernate check the Hibernate docs. The Criteria API is comes from JPA so there might be some tutorials around for that. – M. Deinum Nov 29 '13 at 10:31
  • A great project such as [specification-arg-resolver](https://github.com/tkaczmarzyk/specification-arg-resolver) can be used. Read the blog [post](https://blog.tratif.com/2017/11/23/effective-restful-search-api-in-spring/). – rmalviya Aug 25 '20 at 06:02