I usually post about REST APIs and HTTP on Google+ - you can follow me there:
1. Overview
In this first article we’ll explore a simple query language for a REST API. We’ll make good use of Spring for the REST API and JPA 2 Criteria for the persistence aspects.
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 put forward the simple entity that we’re going to use for our filter/search API – a basic User:
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }
3. Filter using CriteriaBuilder
Now – let’s get into the meat of the problem – the query in the persistence layer.
Building a query abstraction is a matter of balance. We need a good amount of flexibility on the one hand, and we need to keep complexity manageable on the other. High level, the functionality is simple – you pass in some constraints and you get back some results.
Let’s see how that works:
@Repository public class UserDAO implements IUserDAO { @PersistenceContext private EntityManager entityManager; @Override public List<User> searchUser(List<SearchCriteria> params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = builder.createQuery(User.class); Root r = query.from(User.class); Predicate predicate = builder.conjunction(); for (SearchCriteria param : params) { if (param.getOperation().equalsIgnoreCase(">")) { predicate = builder.and(predicate, builder.greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase("<")) { predicate = builder.and(predicate, builder.lessThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase(":")) { if (r.get(param.getKey()).getJavaType() == String.class) { predicate = builder.and(predicate, builder.like(r.get(param.getKey()), "%" + param.getValue() + "%")); } else { predicate = builder.and(predicate, builder.equal(r.get(param.getKey()), param.getValue())); } } } query.where(predicate); List<User> result = entityManager.createQuery(query).getResultList(); return result; } @Override public void save(User entity) { entityManager.persist(entity); } }
As you can see, the searchUser API takes a list of very simple constraints, composes a query based on these constrains, does the search and returns the results.
The constraint class is quite simple as well:
public class SearchCriteria { private String key; private String operation; private Object value; }
The SearchCriteria implementation holds our Query parameters:
- key: used to hold field name – for example: firstName, age, … etc.
- operation: used to hold the operation – for example: Equality, less than, … etc.
- value: used to hold the field value – for example: john, 25, … etc.
4. Test the Search Queries
Now – let’s test our search mechanism to make sure it holds water.
First – let’s initialize our database for testing by adding two users – as in the following example:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @TransactionConfiguration public class JPACriteriaQueryTest { @Autowired private IUserDAO userApi; private User userJohn; private User userTom; @Before public void init() { userJohn = new User(); userJohn.setFirstName("John"); userJohn.setLastName("Doe"); userJohn.setEmail("john@doe.com"); userJohn.setAge(22); userApi.save(userJohn); userTom = new User(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("tom@doe.com"); userTom.setAge(26); userApi.save(userTom); } }
Now, let’s get a User with specific firstName and lastName – as in the following example:
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "John")); params.add(new SearchCriteria("lastName", ":", "Doe")); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
Next, let’s get a List of User with the same lastName:
@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("lastName", ":", "Doe")); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }
Next, let’s get users with age greater than or equal 25:
@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("lastName", ":", "Doe")); params.add(new SearchCriteria("age", ">", "25")); List<User> results = userApi.searchUser(params); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
Next, let’s search for users that don’t actually exist:
@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "Adam")); params.add(new SearchCriteria("lastName", ":", "Fox")); List<User> results = userApi.searchUser(params); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); }
Finally, let’s search for users given only partial firstName:
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "jo")); List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
6. The UserController
Finally, let’s now wire in the persistence support for this flexible search to our REST API.
We’re going to be setting up a simple UserController – with a findAll() using the “search” to pass in the entire search/filter expression:
@Controller public class UserController { @Autowired private IUserDao api; @RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List<User> findAll(@RequestParam(value = "search", required = false) String search) { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); if (search != null) { Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { params.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); } } return api.searchUser(params); } }
Note how we’re simply creating our search criteria objects out of the search expression.
We’re not at the point where we can start playing with the API and make sure everything is working correctly:
http://localhost:8080/users?search=lastName:doe,age>25
And here is its response:
[{ "id":2, "firstName":"tom", "lastName":"doe", "email":"tom@doe.com", "age":26 }]
7. Conclusion
This simple yet powerful implementation enables quite a bit of smart filtering on a REST API. Yes – it’s still rough around the edges and can be improved (and will be improved in the next article) – but it’s a solid starting point to implement this kind of filtering functionality on your APIs.
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.