Searching and Filtering: Spring Data JPA Specification way

Rahul Yadav
fleetx engineering
Published in
5 min readJul 27, 2020

Searching and filtering is one of the most trivial operations which can be performed on a data set. From selecting your favourite brand name while shopping on Amazon to searching for articles based on a keyword on Medium, almost every application has some kind of a use case for this functionality.

At Fleetx as well, searching and filtering is done for a wide range of data sets.

Simply put: A filter is analogous to a where clause in a typical database query.

Problem Statement

During my initial days at Fleetx, I came across a problem while working on a feature. I had to provide a filter for a new field which was added in an existing database entity. To give you a little bit of background, filtering was implemented by writing a separate query for each combination of possible filters. For instance, consider the below entity and its corresponding dao repository:

@Data
@Entity
public class User {

@Id
@GeneratedValue
private Long id;

private String name;

@ManyToOne
@JoinColumn(name = "account_id", updatable = false)
private Account account;
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByAccountId(Long accountId); List<User> findByName(String name); List<User> findByAccountIdAndName(Long accountId, String name);}

As we can see, for every field, there was one query for filtering on that particular field and queries with all possible combinations with the rest of the fields. So in this implementation, if a new field is added, for instance groupId, we will have to add a whole lot of queries for all the possible new combinations.

There will be a total of 2^n — 1 queries for n fields. Ouch!!

Solution

Luckily, the above problem having exponential complexity can be solved in O(n) (Competitive programmers would be cheering now :P), thanks to the Specification interface of Spring Data JPA. Please be advised that the internal working of Specification is out of the scope of this article (check out the aforementioned link). We will see how we integrated this simple and efficient framework in our application.

Our own Specification

Specification can be considered analogous to the where clause of a database query. The first part consisted of creating a generic implementation of the Specification interface which will be able to handle different type of operations which are typically used to filter a data set.

@Slf4j
public class GenericSpecification<T> implements Specification<T> {

private SearchCriteria searchCriteria;

public GenericSpecification(final SearchCriteria searchCriteria){
super();
this.searchCriteria = searchCriteria;
}

@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Object> arguments = searchCriteria.getArguments();
Object arg = arguments.get(0);

switch (searchCriteria.getSearchOperation()) {
case EQUALITY:
return criteriaBuilder.equal(root.get(searchCriteria.getKey()), arg);
case GREATER_THAN:
return criteriaBuilder.greaterThan(root.get(searchCriteria.getKey()), (Comparable) arg);
... case IN:
return root.get(searchCriteria.getKey()).in(arguments);
}
}
}
@Data
public class SearchCriteria {

private String key;
private SearchOperation searchOperation;
private boolean isOrOperation;
private List<Object> arguments;
}
public enum SearchOperation {

EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH,
...
}

As we can see, the toPredicate method returns a predicate based on the operation that needs to be performed on a particular field. We use the standard functions of CriteriaBuilder and Root interface to create the predicates for a particular criteria. The key field defines the criteria on which the filtering should take place.

Building Specifications

Now that we have a generic Specification ready with us, we need to have a way to build different specifications and combine them together to execute a single query.

public class GenericSpecificationsBuilder<T> {

private final List<SearchCriteria> params;
private final List<Specification<T>> specifications;

public GenericSpecificationsBuilder() {
this.params = new ArrayList<>();
this.specifications = new ArrayList<>();
}

public final GenericSpecificationsBuilder<T> with(final String key, final SearchOperation searchOperation, final List<Object> arguments) {
return with(key, searchOperation, false, arguments);
}

public final GenericSpecificationsBuilder<T> with(final String key, final SearchOperation searchOperation, final boolean isOrOperation, final List<Object> arguments) {
params.add(new SearchCriteria(key, searchOperation, isOrOperation, arguments));
return this;
}
public final GenericSpecificationsBuilder<T> with(Specification<T> specification) {
specifications.add(specification);
return this;
}
public Specification<T> build() {
Specification<T> result = null;
if (!params.isEmpty()) {
result = new GenericSpecification<>(params.get(0));
for (int index = 1; index < params.size(); ++index) {
SearchCriteria searchCriteria = params.get(index);
result = searchCriteria.isOrOperation() ? Specifications.where(result).or(new GenericSpecification<>(searchCriteria))
: Specifications.where(result).and(new GenericSpecification<>(searchCriteria));
}
}
if (!specifications.isEmpty()) {
int index = 0;
if (Objects.isNull(result)) {
result = specifications.get(index++);
}
for (; index < specifications.size(); ++index) {
result = Specifications.where(result).and(specifications.get(index));
}
}
return result;
}
}

As its evident, theGenericSpecificationsBuilder is based on the Builder design pattern which adds specifications and in the build function, creates a single unified Specification combining all the different criteria into one single predicate.

Although, we can use the above builder directly, I created a factory as well which wraps all the possible search operations that are supported under the hood.

@Component
public class SpecificationFactory<T> {

public Specification<T> isEqual(String key, Object arg) {
GenericSpecificationsBuilder<T> builder = new GenericSpecificationsBuilder<>();
return builder.with(key, SearchOperation.EQUALITY, Collections.singletonList(arg)).build();
}
... public Specification<T> isGreaterThan(String key, Comparable arg) {
GenericSpecificationsBuilder<T> builder = new GenericSpecificationsBuilder<>();
return builder.with(key, SearchOperation.GREATER_THAN, Collections.singletonList(arg)).build();
}
}

The above factory contains the basic implementation for all the operations that are supported by SearchOperation defined in SearchCriteria class.

Putting it together

Now that we have our builder and factory in place, let’s see how to actually use them. To enable use of Specification, we have to make our repository extend the JpaSpecificationExecutor interface which provides apis for using Specification.

@Service
public UserService implements IUserService {
@Autowired
private SpecificationFactory<User> userSpecificationFactory;
... @Override
public Page<User> searchUsers(Pageable pageable, Long accountId, String name) {
GenericSpecificationsBuilder<User> builder = new GenericSpecificationsBuilder<>(); if (Objects.nonNull(accountId)) {
builder.with(userSpecificationFactory.isEqual("account", accountId));
}
if (StringUtils.isNotEmpty(name)) {
builder.with(userSpecificationFactory.isLike("name", name));
}
return userRepository.findAll(builder.build(), pageable);
}
}@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}

Now, if we need to add a filter based on a new criteria, all we have to do is wrap the specification criteria in another if statement and viola, that’s it. Pretty neat :)

Pitfalls

One of the drawbacks of using the above implementation is that we no longer have type safety. For instance, a runtime exception will be thrown if:

  • Argument mismatch: For instance, a field of type int is passed a String value as an argument.
  • Field not found: A specification is based on a field that does not exist in the entity.

Conclusion

The problem of searching and filtering is trivial to all modern day applications and the Spring Data JPA Specification provides a neat and elegant way to create dynamic queries. Please share your thoughts and suggestions on how you would like to solve the problem of searching and filtering.

Sources

--

--