- REST Query Language with Spring Data JPA Specifications
- Get started with Spring 5 and Spring Boot 2, through the reference Learn Spring course:
- Get started with Spring Data JPA through the reference Learn Spring Data JPA course:
- 1. Overview
- 2. User Entity
- 3. Filter Using Specification
- 4. The UserRepository
- 5. Test the Search Queries
- 6. Combine Specifications
- 7. UserController
- 8. Conclusion
REST Query Language with Spring Data JPA Specifications
The Kubernetes ecosystem is huge and quite complex, so it’s easy to forget about costs when trying out all of the exciting tools.
To avoid overspending on your Kubernetes cluster, definitely have a look at the free K8s cost monitoring tool from the automation platform CAST AI. You can view your costs in real time, allocate them, calculate burn rates for projects, spot anomalies or spikes, and get insightful reports you can share with your team.
Connect your cluster and start monitoring your K8s costs right away:
We rely on other people’s code in our own work. Every day.
It might be the language you’re writing in, the framework you’re building on, or some esoteric piece of software that does one thing so well you never found the need to implement it yourself.
The problem is, of course, when things fall apart in production — debugging the implementation of a 3rd party library you have no intimate knowledge of is, to say the least, tricky.
Lightrun is a new kind of debugger.
It’s one geared specifically towards real-life production environments. Using Lightrun, you can drill down into running applications, including 3rd party dependencies, with real-time logs, snapshots, and metrics.
Learn more in this quick, 5-minute Lightrun tutorial:
Slow MySQL query performance is all too common. Of course it is. A good way to go is, naturally, a dedicated profiler that actually understands the ins and outs of MySQL.
The Jet Profiler was built for MySQL only, so it can do things like real-time query performance, focus on most used tables or most frequent queries, quickly identify performance issues and basically help you optimize your queries.
Critically, it has very minimal impact on your server’s performance, with most of the profiling work done separately — so it needs no server changes, agents or separate services.
Basically, you install the desktop application, connect to your MySQL server, hit the record button, and you’ll have results within minutes:
DbSchema is a super-flexible database designer, which can take you from designing the DB with your team all the way to safely deploying the schema.
The way it does all of that is by using a design model, a database-independent image of the schema, which can be shared in a team using GIT and compared or deployed on to any database.
And, of course, it can be heavily visual, allowing you to interact with the database using diagrams, visually compose queries, explore the data, generate random data, import data or build HTML5 database reports.
The Kubernetes ecosystem is huge and quite complex, so it’s easy to forget about costs when trying out all of the exciting tools.
To avoid overspending on your Kubernetes cluster, definitely have a look at the free K8s cost monitoring tool from the automation platform CAST AI. You can view your costs in real time, allocate them, calculate burn rates for projects, spot anomalies or spikes, and get insightful reports you can share with your team.
Connect your cluster and start monitoring your K8s costs right away:
Get started with Spring 5 and Spring Boot 2, through the reference Learn Spring course:
Get started with Spring Data JPA through the reference Learn Spring Data JPA course:
We’re looking for a new Java technical editor to help review new articles for the site.
1. Overview
In this tutorial, we’ll 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 searching/filtering our resources by very simple fields just isn’t enough for APIs that are too complex. A query language is more flexible and allows us to filter down to exactly the resources we need.
2. User Entity
First, let’s start with a simple User entity for our Search API:
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 that implements the Specification interface, and we’re going to pass in our own constraint to construct the actual query:
public class UserSpecification implements Specification < private SearchCriteria criteria; @Override public Predicate toPredicate (Rootroot, CriteriaQuery query, CriteriaBuilder builder) < if (criteria.getOperation().equalsIgnoreCase(">")) < return builder.greaterThanOrEqualTo( root.get(criteria.getKey()), criteria.getValue().toString()); > else if (criteria.getOperation().equalsIgnoreCase(" <")) < return builder.lessThanOrEqualTo( root.get(criteria.getKey()), criteria.getValue().toString()); > else if (criteria.getOperation().equalsIgnoreCase(":")) < if (root.get(criteria.getKey()).getJavaType() == String.class) < return builder.like( root.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 constraints that we represent in the following SearchCriteria class:
public class SearchCriteria
The SearchCriteria implementation holds a basic representation of a constraint, and it’s based on this constraint that we’re going construct 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. However, it’s 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, JpaSpecificationExecutor <>
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 JPASpecificationIntegrationTest < @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")); Listresults = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); >
Now we’ll 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")); Listresults = 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 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 results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); >
Now we’ll see how to search for a 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")); Listresults = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); >
Finally, we’ll find a User given only part of the first name:
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() < UserSpecification spec = new UserSpecification(new SearchCriteria("firstName", ":", "jo")); Listresults = 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. But before let’s check — SpecSearchCriteria — object:
public class SpecSearchCriteria < private String key; private SearchOperation operation; private Object value; private boolean orPredicate; public boolean isOrPredicate() < return orPredicate; >>
public class UserSpecificationsBuilder < private final Listparams; public UserSpecificationsBuilder() < params = new ArrayList<>(); > public final UserSpecificationsBuilder with(String key, String operation, Object value, String prefix, String suffix) < return with(null, key, operation, value, prefix, suffix); >public final UserSpecificationsBuilder with(String orPredicate, String key, String operation, Object value, String prefix, String suffix) < SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0)); if (op != null) < if (op == SearchOperation.EQUALITY) < // the operation may be complex operation boolean startWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX); boolean endWithAsterisk = suffix != null && suffix.contains(SearchOperation.ZERO_OR_MORE_REGEX); if (startWithAsterisk && endWithAsterisk) < op = SearchOperation.CONTAINS; >else if (startWithAsterisk) < op = SearchOperation.ENDS_WITH; >else if (endWithAsterisk) < op = SearchOperation.STARTS_WITH; >> params.add(new SpecSearchCriteria(orPredicate, key, op, value)); > return this; > public Specification build() < if (params.size() == 0) return null; Specification result = new UserSpecification(params.get(0)); for (int i = 1; i < params.size(); i++) < result = params.get(i).isOrPredicate() ? Specification.where(result).or(new UserSpecification(params.get(i))) : Specification.where(result).and(new UserSpecification(params.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 Listsearch(@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 spec = builder.build(); return repo.findAll(spec); > >
Note that to support other non-English systems, the Pattern object could be changed:
Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);
Here is a test URL to test out the API:
http://localhost:8080/users?search=lastName:doe,age>25
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, we can consider using a different separator such as “;”.
Another option would be to change the pattern to search for values between quotes and then strip these from the search term:
Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\"([^\"]+)\")");
8. Conclusion
This article 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.