The new Certification Class of REST With Spring is out:

>> 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 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.

Go deeper into building a REST API with Spring:

>> CHECK OUT THE CLASSES

  • 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

    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);

  • 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

  • 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

  • Anchit Pancholi

    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 ?

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

  • Mauricio Sanabria rivera

    public UserSpecificationsBuilder with(String key, String operation, Object value) {

    params.add(new SearchCriteria(key, operation, value));

    return builder;

    }

    I am not sure what the builder variable is, it is not defined in the UserSpecificationsBuilder class, could someone, help me with this?
    Thanks in advance

    • Hey Mauricio – should have been this not builder – thanks (fixed). Cheers,
      Eugen.

  • Manish Kumar

    Hi Eugen, thanks for such a nice and easily understandable article!
    I got confused by looking at Spring doc and their official blog but this definitely helped me and implemented similar to this. But I have a question about multiple specifications implementation. I implemented as follows is my approach not efficient

    @Override
    public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) {

    List predicates = new ArrayList();

    if (!StringUtils.isEmpty(criteria.getBrand())) {
    predicates.add(builder.equal(root.get(“brand”).get(“name”), criteria.getBrand()));
    }
    if (!StringUtils.isEmpty(criteria.getName())) {
    predicates.add(builder.like(builder.upper(root.get(“name”)), “%” + criteria.getName().toUpperCase() + “%”));
    }
    if(criteria.getMax_cost() > 0){
    predicates.add(builder.lessThanOrEqualTo(root.join(“offers”).get(“price”), criteria.getMax_cost()));
    }

    query.distinct(true);
    return builder.and(predicates.toArray(new Predicate[]{}));
    }

    • Hey Manish,
      I’m glad the article was helpful.
      To answer your question here – as you can see, I am using an if-else type of Predicate selection as well; now, you do want to make that very fast, and you certainly don’t want a lot of checks there, but a few is generally OK.
      Of course, ultimately, you’re going to have to test it to get some real performance data to back up your decision, but I would suggest doing that at a broad, high level, not specifically focused on these few if statements.
      Hope that helps. Cheers,
      Eugen.

  • Nitish Raj

    Hi Eugen, this is a great article. I have a scenario where in I need to query into table with parameters in joined tables. Example: User entity has joined entity UserName (id, firstName, lastName). Now, I need to search list of users based on lastName. How can I achieve that ?

    Thanks,
    Nitish

    • Hey Nitish,
      I’m glad you enjoyed the writeup. To answer your question – you’d simply write the SQL query first, and then see how easily it maps to using Specifications. If it doesn’t easily map – have a look at the other articles in the series (there are several using different persistence technologies, not just Specifications).
      Hope that points you in the right direction. Cheers,
      Eugen.

  • Andreas Schöppe

    HI Eugen,

    I got this error message about the match pattern. Can you help me out ?

    Invalid escape sequence (valid ones are b t n f r ” ‘ \ )

    (“(w+?)(:|)(w+?),”);

    Thanks,Andreas

    • Hey Andreas, I’ll have a look, sure – but I’ll need a failing test for that – so, feel free to open a PR and send me the link.
      Cheers,
      Eugen.

    • Shiva Amrit

      Hey Andreas, please use a double slash as that is how the Java compiler escapes it.

  • Fakhrud ZD Saputra

    Hello Eugen,

    I already implemented your article with some modification for Date type. Now I want to search using nested object since my entity holding reference to another entity as One to One relation. I search it using something like: search=ratingDetail.totalRating>4

    But I got this error message:
    Unable to locate Attribute with the the given name [ratingDetail.totalRating] on this ManagedType [com.project.entity.UserDetail]

    The UserDetail is the entity which holding reference to RatingDetail using One to One. Could you give me some advice? I am totally new using Specification. Thank you for your time.

    • Hey ZD,
      That’s an interesting question, but it’s unfortunately going to be difficult to answer without looking at the code. The way to go here would be a PR on Github with a failing test – and I’d be happy to have a look.
      Hope that helps. Cheers,
      Eugen.