An alternative API for filtering data with Spring MVC & Spring Data JPA

Update It's been a while since I wrote this post. I still consider it worth reading, but please be sure to check the Github page as well, because the described library has evolved and has become a full-fledged open source project available in Maven Central.


I firmly believe that a framework which is reaching excellence eventually becomes some kind of a (domain specific) language.

I've been using Spring MVC for several years now, but I'm still really impressed by the flexiblity of the way you define controllers. It's no longer just a bunch of interfaces to implement, but rather a set of building blocks that enable programmers to fluently express their web application. It's even better, because Spring gives us an API to extend it.

Spring Data Web is a good example of using this API in order to extend Spring MVC controller definition "language". For example, it provides a HandlerMethodArgumentResolver which allows you to use a Pageable object as an argument of your controller method. That's pretty cool -- no need for manual binding of the HTTP parameters to handle pagination, which is something present in virtually all enterprise systems.

Actually, there is one more feature (beside sorting and paging) that we usually have to implement when dealing with tabular data. It's data filtering (or searching). With a custom HandlerMethodArgumentResolver it can be handled in a generic and declarative way. I took up the challenge of implementing it and would like to share the result with you.

Filtering by a single attribute

The simplest example is finding entities by a single attribute. Let's assume we have Customer entity and we want to find all customers with the provided first name. The web request could look like this:

GET http://myhost/api/customers?firstName=Homer

With the micro-library I implemented, the corresponding controller method could have the form of:

@RequestMapping(value = "/customers", params = "firstName")
public Iterable<Customer> findByFirstName(  
      @Spec(path = "firstName", spec = Like.class) Specification<Customer> spec) {

    return customerRepo.findAll(spec);
}

As you can see, it's regular Spring MVC and Spring Data plus a custom @Spec annotation. The annotation points to a class (in this case Like) which implements the predicate. The predicate is automatically instantiated by a custom argument resolver specified in the app configuration. The Spring Data repository (customerRepo) uses the provided criteria to execute a query with the following where-clause: where firstName like '%Homer%'.

Let's take a look on the components used in the example:

  1. The argument is of type Specification<Customer>. It's a higher-level wrapper over the JPA criteria API. The interface is provided by Spring Data JPA which derived it from the specification pattern introduced in Eric Evan's Domain Driven Design book.

  2. The @Spec annotation contains metadata which defines the interpretation of web requests. The path attribute specifies the property to filter entities by. By default, the filtering pattern is expected as a value of the web parameter with the same name as the property path (firstName in this case).

    The spec attribute of the annotation points to a Specification implementation to be used. In this case it's Like, which for a specified path would match the provided pattern using like %pattern% where clause.

  3. The method is provided with the Specification argument automatically by Spring MVC and the custom HandlerMethodArgumentResolver. The resolver itself processes the annotation and accesses the web request parameters to instantiate a proper Specification object.

    The actual implementation of the resolver can be found in the repo on Github. I'm not presenting the code here, because it's really just annotation processing and some reflection.

The specs

Let's take a closer look on the Specifications. Like class is implemented as follows:

public class Like<T> extends PathSpecification<T> {

    private String pattern;

    public Like(String path, String... args) {
        super(path);
        if (args == null || args.length != 1) {
            throw new IllegalArgumentException(
                "Expected exactly one argument (a fragment to match against)");
        } else {
            // (caution! some additional validation is required!)
            this.pattern = "%" + args[0] + "%";
        }
    }

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> q, CriteriaBuilder cb) {
        return cb.like(this.<String>path(root), pattern);
    }
}

The important parts are:

  • You may wonder why the constructor doesn't explicitly take a pattern argument but rather an array of strings called args. It's in order to keep the argument resolver independent from any new Specification implementations added in the future (Open/Closed principle). The resolver just takes a class from the spec parameter of the annotation and calls its constructor using reflection. It's not aware of any other details of the class it instantiates, which allows you to add a new Specification freely.

  • toPredicate method uses path and pattern that were taken from the web request (based on the annotation metadata). It's regular JPA criteria code. The path method is implemented in the superclass and its goal is to parse strings like "firstName" or "address.street" into JPA expressions.

  • To keep focus on the general assumptions, I skipped some additional validation. In many cases you would like to check the pattern (and e.g. force at least 3 characters) to avoid too unrestricted queries.

Similarly, we can implement any other specifications to handle queries with different predicates.

When the web parameter is not present

It's worth to ponder what would happen if firstName parameter was not present in the request. Well, the @RequestMapping has this parameter explicitely specified, so it's part of the mapping -- if it's not present, the controller method won't be called. But an alternative would be to annotate the method just like this:

@RequestMapping("/customers") // no params required
...

We could then assume that these requests:

 GET http://myhost/api/customers
 GET http://myhost/api/customers?firstName=Homer

should return all customers and customers filtered by first name respectively.

There is an elegant solution for that. The argument resolver returns null in case the web parameter specified in @Spec is not present. Null specification is handled by Spring Data as empty criteria, hence no filtering at all. So the following method handles both of the cases:

@RequestMapping("/customers") // no 'params' argument
public Iterable<Customer> findByFirstName(  
      @Spec(path = "firstName", spec = Like.class) Specification<Customer> spec) {

    return customerRepo.findAll(spec);
}

When the parameter name is different than the property name

By default, the web parameter name is the same as the property path of the attribute. It's convenient, but sometimes we want to override this rule to make the parameter name more explicit. For example, let's assume we want to find all customers registered before a certain date. The web request could look like:

GET http://myhost/api/customers?registeredBefore=2014-03-10

In this case it's more meaningful to say registeredBefore instead of just using the attribute name (registrationDate). The mapping to handle the above would look like:

@RequestMapping("/customers")
public Iterable<Customer> findByRegistrationDate(  
      @Spec(params = "registeredBefore"
            path = "registrationDate", spec = DateBefore.class) Specification spec) {

    return customerRepo.findAll(spec);
}

As you can see, it uses an additional attribute (params) to specify the name of the web parameter.

Another interesting thing is the specification used in the snippet -- DateBefore. The implementation is very similar to Like described before, but you may wonder about the date-time format. It's a good point! DateBefore class uses some default format which can be overriden with an optional config attribute of the @Spec annotation:

@Spec(path="registrationDate", params="registeredBefore",
      spec=DateBefore.class, config="yyyy-MM-dd")
...

The resolver can then pass such an additional info to the specification constructor.

Using more than one parameter to build a spec

So far we've seen examples using exactly one web parameter per specification. It doesn't have to be like that. A good example of a specification that needs two arguments is date between expression, i.e. where date between :after and :before. It can be easily handled, because params attribute of @Spec can take an array of strings. The controller definition would become just:

@RequestMapping("/customers")
public Iterable<Customer> findByRegistrationDate(  
      @Spec(params = {"registeredBefore", "registeredAfter"},
            path = "registrationDate", spec = DateBetween.class) Specification spec) {

    return customerRepo.findAll(spec);
}

Filtering by multiple attributes

Sometimes we would like to filter by multiple attributes simultaneously. For example this request:

GET http://myhost/api/customers?firstName=Homer&lastName=Simpson

should narrow the list of customers by both firstName and lastName. To handle such a request, the controller method could be declared as follows:

@RequestMapping(value = "/customers/", params = { "firstName", "lastName" })
public Iterable<Customer> find(  
    @And(
      @Spec(path = "firstName", spec = Like.class),
      @Spec(path = "lastName", spec = Like.class)) Specification spec) {

  return customerRepo.find(spec);
}

As you can see, the argument is now annotated with @And, which is a wrapper around multiple @Spec annotations. As a result, the resolved specification translates to where firstName like '%Homer%' and lastName like '%Simpson%'. We could use the @Or annotation similarly, to define a disjunction.

Matching multiple paths against a single web parameter

Often there is a need for a search engine that would match the provided phrase against multiple attributes of an entity. For example, when a user enters "8976" it would first examine customer number, then VAT ID. The result set should include all the customers, whose either one of those attributes contains the given pattern. The search engine is user-friendly enough for the user not to have to explicitly choose the search criteria. The corresponding request could have a form of just:

GET http://myhost/api/customers?query=8976

The API described in the examples above is sufficient to define a method that handles the query:

@RequestMapping(value = "/customers")
public Iterable<Customer> find(  
   @Or({
     @Spec(params="query", path="customerNumber", spec=Like.class),
     @Spec(params="query", path="vatId", spec=Like.class)}) Specification spec){

  return customerRepo.find(spec);
}

That's it. I hope that at this point the above declaration is easy to understand.

More complex queries

The resolver I implemented handles more complex definitions as well -- for example you could have multiple @Or annotations inside an @And. I'm not showing such examples here but you can find them in the repo on Github if you want.

Alternatively, you could have multiple Specification arguments (with different annotations) in a single controller method and then merge them programmatically. You can find a sample of that on Github as well.

Conclusion

In this post I've presented an API for declarative resolution of Specfication arguments of Spring MVC controller handler methods. The presented implementation is a custom HandlerMethodArgumentResolver that uses annotation metadata and web requests parameters. I successfully used it in one of my projects. I think it might be a time saver in the case of finder methods that are not very complex but numerous.

Besides, I think it's an instructive excercise to design and implement such APIs. Spring gives us many mechanisms to extend it for our needs. HandlerMethodArgumentResolver interface is one of them and I encourage you to consider it as a way to reduce the amount of repetitive code in your controllers.

The sources

  1. Te source code of the library can be found on Github: https://github.com/tkaczmarzyk/specification-arg-resolver. It's worth to take a look on the README.md and CHANGELOG.md files, as they might include description of new features introduced after this article had been written.

  2. You can download the binary distribution from Maven Central repository:
    <dependency>
        <groupId>net.kaczmarzyk</groupId>
        <artifactId>specification-arg-resolver</artifactId>
        <version>0.6.0</version>
    </dependency>
    
  3. Alternatively, you can grab the latest snapshot from my private Maven repository:
    <repository>
        <id>kaczmarzyk.net</id>
        <url>http://repo.kaczmarzyk.net</url>
    </repository>
    
    The dependency definition is:
    <dependency>
        <groupId>net.kaczmarzyk</groupId>
        <artifactId>specification-arg-resolver</artifactId>
        <version>0.6.0</version>
    </dependency>
    
  4. A simple web application using the library can be found in this repo: https://github.com/tkaczmarzyk/specification-arg-resolver-example

    It's built on top of Spring Boot, so you don't need any server to run it -- just run it as a standalone java application and it starts listening on http://localhost:8080. It exposes a REST API for customer database filtering.


comments powered by Disqus