I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial – we will build a Search/Filter REST API using Spring Data JPA and Specifications.

We started looking at a query language in the first article of this series – with a JPA Criteria based solution.

So – why a query language? Because – for any complex-enough API – searching/filtering your resources by very simple fields is simply not enough. A query language is more flexible and allows you to filter down to exactly the resources you need.

2. User Entity

First – let’s start with a simple User entity for our Search API:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
    
    // standard getters and setters
}

3. Filter Using Specification

Now – let’s get straight into the most interesting part of the problem – querying with custom Spring Data JPA Specifications.

We’ll create a UserSpecification which implements the Specification interface and we’re going to pass in our own constraint to construct the actual query:

public class UserSpecification implements Specification<User> {

    private SearchCriteria criteria;

    @Override
    public Predicate toPredicate
      (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
 
        if (criteria.getOperation().equalsIgnoreCase(">")) {
            return builder.greaterThanOrEqualTo(
              root.<String> get(criteria.getKey()), criteria.getValue().toString());
        } 
        else if (criteria.getOperation().equalsIgnoreCase("<")) {
            return builder.lessThanOrEqualTo(
              root.<String> get(criteria.getKey()), criteria.getValue().toString());
        } 
        else if (criteria.getOperation().equalsIgnoreCase(":")) {
            if (root.get(criteria.getKey()).getJavaType() == String.class) {
                return builder.like(
                  root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%");
            } else {
                return builder.equal(root.get(criteria.getKey()), criteria.getValue());
            }
        }
        return null;
    }
}

As we can see – we create a Specification based on some simple constrains which we represent in the following “SearchCriteria” class:

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

The SearchCriteria implementation holds a basic representation of a constraint – and it’s based on this constraint that we’re going to be constructing the query:

  • key: the field name – for example, firstName, age, … etc.
  • operation: the operation – for example, equality, less than, … etc.
  • value: the field value – for example, john, 25, … etc.

Of course, the implementation is simplistic and can be improved; it is however a solid base for the powerful and flexible operations we need.

4. The UserRepository

Next – let’s take a look at the UserRepository; we’re simply extending the JpaSpecificationExecutor to get the new Specification APIs:

public interface UserRepository 
  extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}

5. Test the Search Queries

Now – let’s test out the new search API.

First, let’s create a few users to have them ready when the tests run:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceJPAConfig.class })
@Transactional
@TransactionConfiguration
public class JPASpecificationsTest {

    @Autowired
    private UserRepository repository;

    private User userJohn;
    private User userTom;

    @Before
    public void init() {
        userJohn = new User();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("[email protected]");
        userJohn.setAge(22);
        repository.save(userJohn);

        userTom = new User();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repository.save(userTom);
    }
}

Next, let’s see how to find users with given last name:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));
    
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));
}

Now, let’s see how to find a user with given both first and last name:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("firstName", ":", "john"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));
    
    List<User> results = repository.findAll(Specification.where(spec1).and(spec2));

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

Note: We used “where” and “and” to combine Specifications.

Next, let’s see how to find a user with given both last name and minimum age:

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("age", ">", "25"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));

    List<User> results = 
      repository.findAll(Specification.where(spec1).and(spec2));

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

Now, let’s see how to search for User that doesn’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("firstName", ":", "Adam"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "Fox"));

    List<User> results = 
      repository.findAll(Specification.where(spec1).and(spec2));

    assertThat(userJohn, not(isIn(results)));
    assertThat(userTom, not(isIn(results)));  
}

Finally – let’s see how to find a User given only part of the first name:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = 
      new UserSpecification(new SearchCriteria("firstName", ":", "jo"));
    
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

6. Combine Specifications

Next – let’s take a look at combining our custom Specifications to use multiple constraints and filter according to multiple criteria.

We’re going to implement a builder – UserSpecificationsBuilder – to easily and fluently combine Specifications:

public class UserSpecificationsBuilder {
    
    private final List<SearchCriteria> params;

    public UserSpecificationsBuilder() {
        params = new ArrayList<SearchCriteria>();
    }

    public UserSpecificationsBuilder with(String key, String operation, Object value) {
        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

    public Specification<User> build() {
        if (params.size() == 0) {
            return null;
        }

        List<Specification> specs = params.stream()
          .map(UserSpecification::new)
          .collect(Collectors.toList());
        
        Specification result = specs.get(0);

        for (int i = 1; i < params.size(); i++) {
            result = params.get(i)
              .isOrPredicate()
                ? Specification.where(result)
                  .or(specs.get(i))
                : Specification.where(result)
                  .and(specs.get(i));
        }       
        return result;
    }
}

7. UserController

Finally – let’s use this new persistence search/filter functionality and set up the REST API – by creating a UserController with a simple search operation:

@Controller
public class UserController {

    @Autowired
    private UserRepository repo;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    public List<User> search(@RequestParam(value = "search") String search) {
        UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
        Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),");
        Matcher matcher = pattern.matcher(search + ",");
        while (matcher.find()) {
            builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
        }
        
        Specification<User> spec = builder.build();
        return repo.findAll(spec);
    }
}

Note that to support other non-English systems, the Pattern object could be changed like:

Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);

Here is a test URL example to test out the API:

http://localhost:8080/users?search=lastName:doe,age>25

And the response:

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"[email protected]",
    "age":26
}]

Since the searches are split by a “,” in our Pattern example, the search terms can’t contain this character. The pattern also doesn’t match whitespace.

If we want to search for values containing commas, then we can consider using a different separator such as “;”.

Another option would be to change the pattern to search for values between quotes, then strip these from the search term:

Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\"([^\"]+)\")");

8. Conclusion

This tutorial covered a simple implementation that can be the base of a powerful REST query language. We’ve made good use of Spring Data Specifications to make sure we keep the API away from the domain and have the option to handle many other types of operations.

The full implementation of this article can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

Next »
REST Query Language with Spring Data JPA and Querydsl
« Previous
REST Query Language with Spring and JPA Criteria

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

newest oldest most voted
Notify of
Vladimir Stanković
Guest

How would you fetch join two entities by using Specification API?

Eugen Paraschiv
Guest

Unfortunately I’m not sure there’s a way to do that explicitly – you might need to fall back to Criteria for it. Cheers,
Eugen.

Varsha
Guest
Varsha

SetJoin join1 = root.join(Entity1_.embeddedEntity);
now use this join1 to put the conditions etc. e.g.

predicates.add(cb.equal(join1.get(EmbeddedEntity_.field1), value);

Eugen Paraschiv
Guest

Cool – I’ll try these out – thanks.
Eugen.

punksrant
Guest
punksrant

wanted to give something like this to my users ,but is there a standard way to gives such query DSL’s in a more elegant manner say like a json or something like what elasticsearch does

Eugen Paraschiv
Guest

I don’t know of any other standard besides the one I covered in the last article of this series – RSQL. So, nothing JSON based that I know of. Cheers,
Eugen.

punksrant
Guest
punksrant

Thanks a lot Eugen was looking for something like that .

TrollPatrol
Guest
TrollPatrol

hi Eugen, thanks for the detailed article. If I may bother you asking a question : I’m using a custom class wich implements Specification and I would like to make a filter on multiple fields of my employees. the issue is that the overriden method “toPredicate(..)” return a single Predicate to give to my JPA method findAll. I would like to return a Predicate list/array instead. If you have 5 minuts, can you tell me how can I do this (if I can actually do it this way) ? Thanks a lot

Eugen Paraschiv
Guest

Here’s where that’s handled – one of the next articles in this series.
Hope it helps. Cheers,
Eugen.

TrollPatrol
Guest
TrollPatrol

Hi again,
I solved my issue this way : http://stackoverflow.com/questions/33953287/spring-boot-rest-webservice-jpa-pageable-and-filter

Thanks for the reply I ll read this too !