Search code examples
javajpacriteria

easiest way to add parameters to criteria query


I want to find the easiest solution for situation in which you have a query method, which takes some attribute of an entity and based on that attribute performs query which returns that entity.

The question is, if I want to add more attributes by which I can search for entity, what is the easiest solution when using jpa criteria? One aspect here is that the method has to be portable - if I add attributes later, I don't want to rewrite all my code.

I know that this can be done by method overloading, but I'm searching for easier solution.

For example my method's code is:

public List<Car> findCar(String name) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Car> c = cb.createQuery(Car.class);
        Root<Car> d = c.from(Car.class);
        Predicate condition = cb.equal(d.get(Car_.name), name);
        c.where(condition);
        TypedQuery<Car> q = em.createQuery(c);
        List<Car> results = q.getResultList();

        return results;
    }

What should I do if I want to extend this query in a way I can search for Car entity by other attributes, for example, Date. I do not want that this method would take more than one argument.


Solution

  • I am a bit surprised that this question hasn't been discussed here before.

    So I did some investigation and came up with possible answers.

    The easiest way is to have a separate class, f.x. CarSearchFilter in which you can have various filters (i.e. parameters by which you want to perform query):

    public class CarSearchFilter {
    
        private String name;
        private Date date;
        private String color;
        //etc
    
    //getters and setters
    
    }
    

    This way when you want to add new parameters to query, you just add new fields to CarSearhFilter and modify the query method. If you do not modify the query method, it will still work as-is. The API looks good this way. Thus, the findCar method has to be modified this way:

    public List<Car> findCar(CarSearchFilter filter) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Car> c = cb.createQuery(Car.class);
        Root<Car> d = c.from(Car.class);
    
        // you have to add more predicates when you want more parameters
        List<Predicate> predicates = new ArrayList<Predicate>();
        if (filter.getName() != null) {
            predicates.add(cb.like(d.get(Car_.name), filter.getName()));
        }
        if (filter.getDate() != null) {
            predicates.add(cb.lessThanOrEqualTo(d.get(Car_.date), filter.getDate()));
        }
    
        c.select(d).where(predicates.toArray(new Predicate[] {}));
        TypedQuery<Car> q = em.createQuery(c);
        List<Car> results = q.getResultList();
    
        return results;
    }
    

    There are other ways to do this task, too. As Tiny suggested, it is possible to use key/value pair mechanisms to store search filters using keys as property/field names and values as the values of fields, but this approach has some disadvantages:

    • Map is hard-coded
    • You have to store keys somewhere
    • Mistyping can be done
    • etc.