The Price of all “Rest with Spring” course packages will increase by $50 next Friday:

>>> GET ACCESS NOW

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 Specifications

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(Specifications.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(Specifications.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(Specifications.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<User>> specs = new ArrayList<Specification<User>>();
        for (SearchCriteria param : params) {
            specs.add(new UserSpecification(param));
        }

        Specification<User> result = specs.get(0);
        for (int i = 1; i < specs.size(); i++) {
            result = Specifications.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);
    }
}

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
}]

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 an Eclipse based project, so it should be easy to import and run as it is.

The Price of all “Rest with Spring” course packages will increase by $50 next Friday:

>>> GET ACCESS NOW

Sort by:   newest | oldest | most voted
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

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

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

Thanks a lot Eugen was looking for something like that .

TrollPatrol
Guest

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

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 !

Anchit Pancholi
Guest

Very Good tutorial , Thanks For it.
I am trying to execute below query with Spring Specification but i dont how to do it

SELECT Product,
SUM(CASE WHEN reason IN (‘IN’,’REFUND’) THEN Qty
WHEN reason IN (‘OUT’,’WASTE’) THEN -Qty
ELSE NULL END) AS Qty
FROM stock
GROUP BY Product;
Can you please let me ?

Eugen Paraschiv
Guest

Well Anchit – that’s an interesting query to figure out, but it’s certainly way outside the scope for this article.

wpDiscuz