I recently had to try to make an endpoint with Spring Boot that would allow for very flexible querying. This would mean including filtering, sorting, and paging. Most REST endpoints are set up to deal with a single entity so I struggled to find an example/tutorial that included all three. Hopefully, you can benefit from the research that I have done. What I ended up with was a modified version of this tutorial
If you came here for the code then you can just skip down below, or you can jump to the code in Github
Overview
There’s a number of things that have to come together for all of this to work. I’ve broken this down into 5 different sections. I do make some assumptions; however, I will not be going into depth about Spring or JPA. I also assume your data source is SQL based. I know that this example will work for MySQL and SQL Server. However, your mileage may vary as you try different data sources.
Formatting the URL
Normally when making REST endpoints, this is the easiest step. However, due to the complexity of the queries, we have to get a bit creative. Eventually, I ended up with this format:
1 | /?search=columnName:value |
In this example, I expect the endpoint to search my table for where the given column name is equal to the given value.
For my first iteration I included the following operations:
• :
(Equality, =)
• >
(Greater than)
• <
(Less than)
• !
(Negation, not equal)
• -
(Sort descending)
• +
(Sort ascending)
This would mean that a more complex query might look something like:
1 | /?search=col1:val1,col2<val2,col3>val3,col4!val4,col5-,col6+ |
This method worked for me but if you need more functionality you will want to figure that out at this stage as it will define what choices you make with the rest of the service.
Note!: When you’re formatting your URL, match the Java class attributes rather than the table column names! This means that if your column name is first_name but your entity object uses firstName, use firstName in the URL!
Parsing
Next, now that we have a URL defined, we need a way to parse out the search string.
First, we need to define our operations, pretty straight forward enum here.
1 | public enum SearchOperation { |
Next, we need a simple class to house each search criteria:
1 | public class SearchCriteria { |
Then we need to write our parser. Just create a new class. I called mine: SearchCriteriaParser
At the top of that class, I defined the regex that will do most of the parsing.
1 | private static String wordRegex = "[a-zA-Z]\\w*"; |
This looks complex, but it could be simplified for your needs. My input could include things like UUIDs and time stamps which could potentially conflict with recognition of operations. In the end we are looking for a repeating pattern: (word)(operation)(word?). However, that ‘word’ might take different shapes.
Finally, we need to use the regex to parse the search string:
1 | public List<SearchCriteria> parse(String searchString) { |
If we consider our simplified pattern of (word)(operation)(word?) we should only match on strings that match that pattern. Meaning anything that doesn’t match is thrown out.
Note!: Also, you might notice the grouping. In the regex, each group is defined by (). This makes it easier for us when we parse it out. Group 1 will always be the column name, group 2 will always be the operation and group 3 is the optional value. (Sorting doesn’t have a value!)
Building Specifications (Filtering)
Next, we need a way to use this nice list of search criteria. To do this, we need to generate some Specification
objects which are part of javax.persistance
Because we are generating the Specification
s dynamically, it’s easier to implement Specification
s and override the toPredicate
function:
1 | public class SearchSpecification implements Specification<MainObj> { |
After creating a SearchSpecification
with a criterion, the Specification will be able do all the filtering.
What this does behind the scenes is that it actually builds out the where clause of the SQL.
You will also need to make sure that your Repository can use Specifications. Just add a extends JpaSpecificationExecutor<>
.
1 | public interface MainRepository extends JpaRepository<MainObj, UUID>, JpaSpecificationExecutor<MainObj> { |
Ok, so far, we have a way to generate a list of Specification
s but the methods on the Repository
class will only accept one at a time. So, we need a function to and
all the Specification
s together:
1 | private <T, V extends Specification<T>> Optional<Specification<T>> andSpecification(List<V> criteria) { |
Finally, we have a single Specification
. The logic for executing all of that will look like this:
1 | List<SearchCriteria> searchCriteria = searchCriteriaParser.parse(searchString); |
Building Sorts (With Paging)
To use sorting and paging, we need to use a Pageable
. A pageable has three parts: a page size, a page index, and a sort direction.
But first we need to convert our list of search criteria into Sort
.
1 | private List<Sort> generateSortList(List<SearchCriteria> criteria) { |
Then we need to combine all the Sort
s. For this, I wrote a simple function that and
s all the Sort
s together:
1 | private <T, V extends Sort> Optional<Sort> andSort(List<V> criteria) { |
In the end the sorting logic will look like this:
1 | List<Sort> sortList = generateSortList(searchCriteria); |
Combining It All Together
Let’s look at the core service function:
1 | public List<MainObj> search(String searchString, Integer pageSize, Integer pageIndex) { |
Let’s review the steps:
- Parse the search string into
SearchCriteria
- Create
Specification
s for eachSearchCriteria
And
all of theSpecification
s together- Create Sorts for each qualifying criterion
And
all the sorts together- Create
Pageable
- Pass the
Specification
s and thePageable
to theRepository
- Get the content of the page
Still with me? Need to look at the source?
While this service doesn’t have every feature possible, the setup makes it so that you could easily expand on this. Adding more operations would be one simple example. Overall, I’m happy with the way this endpoint turned out.