Spring: Advanced search & filtering

Photo by Glen Noble on UnsplashSpring: Advanced search & filteringMilan BrankovicBlockedUnblockFollowFollowingMay 2Implementing a data access layer of an application has been cumbersome for quite a while.

Too much boilerplate code has to be written to execute simple queries as well as perform pagination, and auditing.

Spring Data JPA was created primarily to improve the implementation of data access layer.

It reduces the amount of boilerplate code required by JPA, which makes the implementation of persistence layer easier and faster.

Spring Data JPA provides a default implementation for each method defined by one of its repository interfaces.

That means that you no longer need to implement basic read or write operations.

On top of that Spring Data JPA provides generation of database queries based on method names (as long as the query is not too complex).

public interface MovieRepository extends JpaRepository<Movie, Long> { List<Movie> findByTitle(String title, Sort sort); Page<Movie> findByYear(Int year, Pageable pageable);}Criteria APIHowever, sometimes, we need to create complex search queries and cannot take advantage of a query generator.

Those queries can be build using the Criteria API and combining predicates.

Criteria API offers a programmatic way to create typed queries, which helps us avoid syntax errors.

Even more, when we use it with Metamodel API, it makes compile-time-checks whether we used the correct field names and types.

Informally, a predicate is a statement that may be true or false depending on the values of its variables.

The Java Predicate interface is a functional interface that is often used as an assignment target for lambda expressions.

LocalDate today = new LocalDate();CriteriaBuilder builder = em.

getCriteriaBuilder();CriteriaQuery<Movie> query = builder.

createQuery(Movie.

class);Root<Movie> root = query.

from(Movie.

class);Predicate isComedy = builder.

equal(root.

get(Movie.

genre), Genre.

Comedy);Predicate isReallyOld = builder.

lessThan(root.

get(Movie.

createdAt), today.

minusYears(25));query.

where(builder.

and(isComedy, isReallyOld));em.

createQuery(query.

select(root)).

getResultList();The main problem with this approach is that predicates are not easy to externalize and reuse because you need to set up the CriteriaBuilder, CriteriaQuery, and Root first.

Also, code readability is poor because it is hard to quickly infer the intent of the code.

SpecificationsTo be able to define reusable Predicates we are going to explore the Specification interface.

It defines a specification as a predicate over an entity which simplifies data access layer implementation even further.

public MovieSpecifications { public static Specification<Movie> isComedy() { return (root, query, cb) -> { return cb.

equal(root.

get(Movie_.

genre), Genre.

Comedy); }; } public static Specification<Movie> isReallyOld() { return (root, query, cb) -> { return cb.

lessThan(root.

get(Movie_.

createdAt), new LocalDate.

now().

minusYears(25)); }; }}We just created the reusable predicates that can be individually executed.

This is not the most beautiful code in the world but it serves the purpose.

To make this even more cleaner each specification can be modeled as a separate specification.

public MovieComedySpecification implements Specification<Movie> { @Override public Predicate toPredicate(Root<Movie> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.

equal(root.

get(Movie_.

genre), Genre.

Comedy);}The next question is: how will we execute these specifications?.To do so, simply extend JpaSpecificationExecutor in the repository interface and thus “pull in” an API to execute Specifications:public interface MovieRepository extends JpaRepository<Movie, Long>, JpaSpecificationExecutor<Movie> { // query methods here}A client can now do:movieRepository.

findAll(MovieSpecifications.

isComedy());movieRepository.

findAll(MovieSpecifications.

isReallyOld());Here, the basic repository implementation will prepare the CriteriaQuery, Root and CriteriaBuilder, apply the Predicate created by the given Specification and execute the query.

But couldn’t we just have created simple query methods to achieve that?Combine SpecificationsWe can combine these individual predicates to meet business requirement.

To do so use and(…) and or(…) methods to concatenate atomic Specifications.

There’s also a where(…) that provides some syntactic sugar to make the expression more readable and a not(…) which negates the given specification.

The simple use case looks like this:movieRepository.

findAll(Specification.

where(MovieSpecifications.

isComedy()) .

and(MovieSpecifications.

isReallyOld()));This improves readability as well as providing additional flexibility as compared to the use of the Criteria API alone.

The only downside here is that coming up with the Specification implementation requires quite some coding effort.

Specification builderIf the business rules for searching are set in stone the above implementation can serve its purpose, but if we have a dynamic queries with combination of multiple constraints that will no longer work easily.

To solve this problem of multiple specifications combination we can introduce a SpecificationBuilder.

The search will be based on the SearchCriteria object which will give us the opportunity to dynamically combine multiple criteria.

public enum SearchOperation { EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE; public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~" }; public static SearchOperation getSimpleOperation(final char input) { switch (input) { case ':': return EQUALITY; case '!': return NEGATION; case '>': return GREATER_THAN; case '<': return LESS_THAN; case '~': return LIKE; default: return null; } }}public class SearchCriteria { private String key; private Object value; private SearchOperation operation;}Now the SpecificationBuilder can look like thispublic final class MovieSpecificationsBuilder { private final List<SearchCriteria> params; public MovieSpecificationsBuilder() { params = new ArrayList<>(); } public Specification<Movie> build() { // convert each of SearchCriteria params to Specification and construct combined specification based on custom rules } public final MovieSpecificationsBuilder with(final SearchCriteria criteria) { params.

add(criteria); return this; }}The MovieSpecificationBuilder is now responsible of creating a specification out of multiple search query criterias based on the bussines rules defined.

The rest of the code does not need to change at all!.The client can now specify criteria and get the result by doing the follwing:final MovieSpecificationsBuilder msb = new MovieSpecificationsBuilder();// add SearchCriteria by invoking with()final Specification<Movie> spec = msb.

build();movieRepository.

findAll(spec);ConclusionThis article covers a simple implementation that can be the base of a powerful REST query language.

By using Spring Data Specifications code can be cleaner and more flexible to support custom user queries.

Also adding a new search criteria with Specifications now becomes a trivial job.

.

. More details

Leave a Reply