Creating Specification<> instances for Spring Data JPA with Spring MVC

September 24, 2014 by Michael

One common requirement in web applications is to be able to filter a given set of data through request parameters. One way to do this is fetch everything from the server and filter it on the client. Me, i’m more of a server guy and i tend to do this inside a database.

Many projects of mine are developed with JPA based beans for the data model and recently i’ve come to really love Spring Data JPA. With the plain declaration of an interface extending a repository interface, you get everything you need to query your domain. Through in a second interface and you can execute JPA based criteria queries:

import entities.Projekt;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
public interface ProjektRepository extends JpaRepository<Projekt, Long>, JpaSpecificationExecutor {    
}

My goal: Create controller methods for Spring MVC like so

@RequestMapping(value = "", method = GET)
public Page<Projekt> getProjekte(
    final @RequestParam(defaultValue = "0", required = false) int page,
    final @RequestParam(defaultValue = "10", required = false) int pageSize,	    
    final @RequestParam(required = false, defaultValue = "lieferjahr") String orderBy,
    final @RequestParam(required = false, defaultValue = "ASC") Direction orderByDirection,
    final @FilterDefinition(paths = FilteredPathsOnProjekt.class) Specification<Projekt> filter	    
) {
    return this.projektRepository.findAll(filter, new PageRequest(page, pageSize, new Sort(orderByDirection, orderBy)));
}

where the filter parameter is of type Specification and where i can define which fields are to be filtered in which way in a type-safe way that is checked on compile time like so:

public static class FilteredPathsOnProjekt<Projekt> extends FilteredPaths<Projekt> {
    public FilteredPathsOnProjekt() {	    
        filter(Projekt_.lieferjahr)
            .with((cb, p, value) -> cb.like(cb.function("to_char", String.class, p, cb.literal("YYYY")), value + "%" ))
        .filter(Projekt_.angelegtAm)
            .with((cb, p, value) -> cb.like(cb.function("to_char", String.class, p, cb.literal("DD.MM.YYYY")), "%" + value + "%"))
        .filter(Projekt_.anwender, Anwender_.name)
            .with((cb, p, value) -> cb.like(cb.lower((Path<String>)p), "%" + value.toLowerCase() + "%"))
        .filter(Projekt_.marktteilnehmer, Marktteilnehmer_.name)
            .with((cb, p, value) -> cb.like(cb.lower((Path<String>)p), "%" + value.toLowerCase() + "%"))
        .filter(Projekt_.versorgungsart)
            .with((cb, p, value) -> cb.like(cb.lower((Path<String>)p), "%" + value.toLowerCase() + "%"));		
    }
}

I found this very inspiring post An alternative API for filtering data with Spring MVC & Spring Data JPA by Tomasz and here again, i big thank you.

My solution is similar, but uses the JPA Metamodel API that generates a metamodel from the annotated entity classes. In the above example, the entity is Projekt, the metamodel Projekt_ that lists all attributes that are mapped from the database.

Let’s start with the controller. Parameter filter is of type Specification and is annotated with @FilterDefinition:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface FilterDefinition {
    Class<? extends FilteredPaths> paths();
 
    Class<? extends FilterSpecification> implementation() default FilterSpecification.class;
}

I need to wrap the paths i want to filter with a class because only are small list of types are allowed in annotations. The class looks like this:

import java.util.List;
import java.util.Map;
import javax.persistence.metamodel.SingularAttribute;
 
public abstract class FilteredPaths<T> {
    protected final FilteredPathsBuilder<T> builder = new FilteredPathsBuilder<>();
 
    public final Map<List<SingularAttribute<?, ?>>, PathOperation<T>> getValue() {
        return this.builder.getFilterablePaths();
    }
 
    protected FilteredPathsBuilder<T> filter(SingularAttribute<?, ?>... path) {
        return builder.filter(path);
    }
}

The core of this is the getValue method. It returns a map of lists of SingularAttributes (a list of them to do queries on sub elements) and a PathOperation. PathOperation is a functional interface:

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;
 
@FunctionalInterface
public interface PathOperation<T> {
    public Predicate buildPredicate(CriteriaBuilder cb, Path<?> path, String value);
}

that is used by my FilterSpecification to build the predicate:

import java.util.Map;
import java.util.function.Function;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
 
import static java.util.stream.Collectors.reducing;
 
public class FilterSpecification<T> implements Specification<T> {
 
    protected final Map<PathOperation, PathAndValue> filteredPaths;
 
    public FilterSpecification(Map<PathOperation, PathAndValue> filteredPaths) {
        this.filteredPaths = filteredPaths;
    }
 
    @Override
    public final Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        this.customizeQuery(root, query);
        return this.filteredPaths.entrySet().stream()
            .map(entry -> entry.getKey().buildPredicate(cb, entry.getValue().getPath(root), entry.getValue().getValue()))
            .collect(
                reducing(cb.conjunction(), Function.<Predicate>identity(), (p1, p2) -> cb.and(p1, p2))
            );
    }
 
    protected void customizeQuery(Root<T> root, final CriteriaQuery<?> query) {
    }
}

As you see there’s one big difference to Tomasz solution: I don’t support OR’ing the filters, i’ve only support AND. I like the way i can use the Java 8 stream api and the reduction method to add them together very much.

How get’s this class instantiated? Here come’s the additional Spring MVC argument resolver:

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.core.MethodParameter;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
 
public class FilteringSpecificationArgumentResolver implements HandlerMethodArgumentResolver {
 
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(FilterDefinition.class) && parameter.getParameterType() == Specification.class;
    }
 
    @Override
    public Object resolveArgument(
        MethodParameter parameter,
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest,
        WebDataBinderFactory binderFactory
    ) throws Exception {
        final FilterDefinition filterDefinition = parameter.getParameterAnnotation(FilterDefinition.class);	
        final FilteredPaths<?> paths = filterDefinition.paths().newInstance();
 
        final Map<PathOperation<?>, PathAndValue> filterValues = new HashMap<>();
        paths.getValue().entrySet().forEach(entry -> {
            final String param = entry.getKey().stream().map(SingularAttribute::getName).collect(Collectors.joining("."));
            final String paramValue = Optional.ofNullable(webRequest.getParameter(param)).orElse("");
            if (!paramValue.isEmpty()) {
                filterValues.put(entry.getValue(), new PathAndValue(entry.getKey(), paramValue));
            }
        });
 
        return filterDefinition.implementation().getConstructor(Map.class).newInstance(filterValues);
    }
}

It supports controller arguments of type Specification annotated with @FilterDefinition. To resolver the argument it gets the annotation and then instantiates the class that holds the lists of paths that should be filtered. The lists actually contains the path and the filter operation. It’s iterated and each list of singular attributes is joined together with a “.”, so one attribute becomes “versorgungsart” and a nested becomes “marktteilnehmer.name” for example. It then checks the web requests if there are request params with those names. If so, the filter operation is added together with the path and the value for that path (from the request) to a new map (the operations are the key). This map than is passed to the implementation of the Specification.

What’s nice here: I don’t need to check if the attributes that are requested from the outside exists. As i start with the attributes of the entity from the start, there’s no chance i try to access one that doesn’t exists.

What’s missing is the pair type PathAndValue

import java.util.List;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.SingularAttribute;
 
public class PathAndValue {
 
    private final List<SingularAttribute<?, ?>> path;
    private final String value;
 
    public PathAndValue(List<SingularAttribute<?, ?>> path, String value) {
        this.path = path;
        this.value = value;
    }
 
    public String getValue() {
        return value;
    }
 
    public Path<?> getPath(final Root<?> root) {
        Path<?> rv = root;
        for (SingularAttribute attribute : path) {
            rv = rv.get(attribute);
        }
        return rv;
    }
}

that not only holds both the path (as a list of attributes) to be filtered and value to be filtered with, but also generates a complete JPA criteria path that is given to the PathOperation.

And last but not least is the little FilteredPathsBuilder that allows me to write the filter definition in a readable way and not obstructed with map and list operations as shown in FilteredPathsOnProjekt above:

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.metamodel.SingularAttribute;
 
public class FilteredPathsBuilder<T> {
    private final Map<List<SingularAttribute<?, ?>>, PathOperation<T>> filterablePaths = new HashMap<>();
 
    private List<SingularAttribute<?, ?>> lastPath;
    private PathOperation<T> lastOp;
 
    public FilteredPathsBuilder<T> filter(SingularAttribute<?, ?>... path) {
        if(lastPath != null && lastOp != null) {
            filterablePaths.put(lastPath, lastOp);	    
        }
        this.lastPath = Collections.unmodifiableList(Arrays.asList(path));
        this.lastOp = null;
        return this;
    }
 
    public FilteredPathsBuilder<T> with(PathOperation<T> op) {
        this.lastOp = op;
        return this;
    }
 
    public Map<List<SingularAttribute<?, ?>>, PathOperation<T>> getFilterablePaths() {
        if(lastPath != null && lastOp != null) {
            filterablePaths.put(lastPath, lastOp);	    
        }
        this.lastPath = null;
        this.lastOp = null;
        return Collections.unmodifiableMap(filterablePaths);
    }    
}

As you can see in FilteredPathsOnProjekt above i have now a little DSL that i use to define which attribute of my entity should be filtered. Writing down the PathOperation as lambdas and using the generated MetaModel to specify the attributes i have not only filters that are checked on compile time but also have the the attributes definition only in one place. All solutions that require writing down lists of request parameters or fiddling with map parameters are too error prone and in the end, way too much work to write.

The above solution works fine with Spring 4.0.x and 4.1.x and certainly with Spring Boot. I’ve got tests for all of the code above and should anyone find this solution useful, i’m gonna prepare a standalone project with it.

Let me know, what you think.

13 comments

  1. xenx wrote:

    Can you share your full code in git ? I want implements your solution, but I have problem

    Posted on August 3, 2015 at 10:41 PM | Permalink
  2. Michael wrote:

    A solution based on my ideas is gonna make it into Spring Data Commons, see https://jira.spring.io/browse/DATACMNS-669. Apart from that, the code is pretty much complete.

    Posted on August 3, 2015 at 10:43 PM | Permalink
  3. Lance wrote:

    Wow, this is so sleek and simple. Thanks for this, works perfect!

    Posted on December 7, 2015 at 11:39 PM | Permalink
  4. Michael wrote:

    Thanks, Lance!

    Glad you like it. Have you seen that the recent Spring Data release has something similar already built in based on my idea?
    See https://spring.io/blog/2015/09/04/what-s-new-in-spring-data-release-gosling “Querydsl web support”
    I’m not using QueryDSL, so i’m staying with my solution.

    Have a great day,
    Michael.

    Posted on December 8, 2015 at 9:19 AM | Permalink
  5. Oscar wrote:

    Hi, thanks for the post.

    I’m trying to implement your solution but I got an error, maybe I’m doing something wrong.

    This is the error

    java.lang.IllegalArgumentException: Invoked method public abstract javax.persistence.criteria.Predicate org.springframework.data.jpa.domain.Specification.toPredicate(javax.persistence.criteria.Root,javax.persistence.criteria.CriteriaQuery,javax.persistence.criteria.CriteriaBuilder) is no accessor method!
    

    The only thing I change of your code was this on my controller. Is it ok?

    @RequestMapping(value = "buscar", method = RequestMethod.GET)
        public Page getProjekte(
                final @RequestParam(defaultValue = "0", required = false) int page,
                final @RequestParam(defaultValue = "10", required = false) int pageSize,
                final @RequestParam(required = false, defaultValue = "numeroOrden") String orderBy,
                final @RequestParam(required = false, defaultValue = "ASC") Sort.Direction orderByDirection,
                final @FilterDefinition(paths = FilteredPaths.class) Specification filter
        ) {
            return this.ordenCompraRepository.findAll(filter, new PageRequest(page, pageSize, new Sort(orderByDirection, orderBy)));
        }

    And I don’t know if the resolver you create is being used. Does it need a annotation or something to be considered?

    I would really appreciate your help.

    Thanks!

    Posted on January 29, 2017 at 12:58 AM | Permalink
  6. Michael wrote:

    Depends how your class

     FilteredPaths

    looks like…

    To make Spring Boot use the resolver you need something like

    public class WebConfig extends WebMvcConfigurerAdapter {
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            argumentResolvers.add(new FilteringSpecificationArgumentResolver());
        }

    But it seems like the resolver is being used. At least toPredicate is called. But without seeing the whole code it’s hard to say.

    I should update the post with my library, i guess.

    Posted on January 29, 2017 at 9:41 AM | Permalink
  7. Oscar wrote:

    My FilteredPaths:

    import java.util.List;
    import java.util.Map;
    import javax.persistence.metamodel.SingularAttribute;

    public abstract class FilteredPaths {
    protected final FilteredPathsBuilder builder = new FilteredPathsBuilder();

    public final Map<List<SingularAttribute>, PathOperation> getValue() {
    return this.builder.getFilterablePaths();
    }

    protected FilteredPathsBuilder filter(SingularAttribute… path) {
    return builder.filter(path);
    }
    }

    Thanks

    Posted on January 30, 2017 at 11:10 PM | Permalink
  8. danbit wrote:

    Michael why don’t you create a project on GitHub?

    Posted on February 20, 2017 at 3:21 PM | Permalink
  9. Michael wrote:

    Mostly timing issues 😉

    Spring Data JPA has this for Query DSL… Maybe I should polish it and send a PR.

    Posted on February 20, 2017 at 5:16 PM | Permalink
  10. Marc wrote:

    why not using reflection?

    Posted on July 19, 2017 at 7:51 PM | Permalink
  11. Michael wrote:

    I wanted to have the type safety of the meta model. It’s already there.

    Posted on July 22, 2017 at 10:09 AM | Permalink
  12. zanareno wrote:

    Hi Michael i am trying to find a way to come up with a solution to my problem. Perhaps you can help me. I’m working on an api that supports filtering like so:
    /resources?filters=name.like.joe, age.gt.12, date.le.20170101.
    How can I use your solution in a way that an endpoint is able to support all options (eq, ne, gt, ge, lt, le) in all eligible filters, since each user can do what he pleases.

    Posted on August 30, 2017 at 5:53 PM | Permalink
  13. Michael wrote:

    Hi,
    I’m gonna check this on the weekend…

    Posted on August 31, 2017 at 9:45 AM | Permalink
Post a Comment

Your email is never published. We need your name and email address only for verifying a legitimate comment. For more information, a copy of your saved data or a request to delete any data under this address, please send a short notice to michael@simons.ac from the address you used to comment on this entry.
By entering and submitting a comment, wether with or without name or email address, you'll agree that all data you have entered including your IP address will be checked and stored for a limited time by Automattic Inc., 60 29th Street #343, San Francisco, CA 94110-4929, USA. only for the purpose of avoiding spam. You can deny further storage of your data by sending an email to support@wordpress.com, with subject “Deletion of Data stored by Akismet”.
Required fields are marked *