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
Can you share your full code in git ? I want implements your solution, but I have problem
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.
Wow, this is so sleek and simple. Thanks for this, works perfect!
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.
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
The only thing I change of your code was this on my controller. Is it ok?
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!
Depends how your class
looks like…
To make Spring Boot use the resolver you need something like
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.
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
Michael why don’t you create a project on GitHub?
Mostly timing issues 😉
Spring Data JPA has this for Query DSL… Maybe I should polish it and send a PR.
why not using reflection?
I wanted to have the type safety of the meta model. It’s already there.
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.
Hi,
I’m gonna check this on the weekend…
Post a Comment