If you are looking for a better way to manage your queries or want to generate dynamic and typesafe queries then you might find your solution in Spring Data JPA Specifications.
Example Code
This article is accompanied by a working code example on GitHub.What Are Specifications?
Spring Data JPA Specifications is yet another tool at our disposal to perform database queries with Spring or Spring Boot.
Specifications are built on top of the Criteria API.
When building a Criteria query
we are required to build and manage Root
, CriteraQuery
, and CriteriaBuilder
objects by ourselves:
...
EntityManager entityManagr = getEntityManager();
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> productQuery = builder.createQuery(Product.class);
Root<Person> personRoot = productQuery.from(Product.class);
...
Specifications build on top of the Criteria API to simplify the developer experience. We simply need to implement the Specification
interface:
interface Specification<T>{
Predicate toPredicate(Root<T> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder);
}
Using Specifications we can build atomic predicates, and combine those predicates to build complex dynamic queries.
Specifications are inspired by the Domain-Driven Design “Specification” pattern.
Why Do We Need Specifications?
One of the most common ways to perform queries in Spring Boot is by using Query Methods like these:
interface ProductRepository extends JpaRepository<Product, String>,
JpaSpecificationExecutor<Product> {
List<Product> findAllByNameLike(String name);
List<Product> findAllByNameLikeAndPriceLessThanEqual(
String name,
Double price
);
List<Product> findAllByCategoryInAndPriceLessThanEqual(
List<Category> categories,
Double price
);
List<Product> findAllByCategoryInAndPriceBetween(
List<Category> categories,
Double bottom,
Double top
);
List<Product> findAllByNameLikeAndCategoryIn(
String name,
List<Category> categories
);
List<Product> findAllByNameLikeAndCategoryInAndPriceBetween(
String name,
List<Category> categories,
Double bottom,
Double top
);
}
The problem with query methods is that we can only specify a fixed number of criteria. Also, the number of query methods increases rapidly as the use cases increases.
At some point, there are many overlapping criteria across the query methods and if there is a change in any one of those, we’ll have to make changes in multiple query methods.
Also, the length of the query method might increase significantly when we have long field names and multiple criteria in our query. Plus, it might take a while for someone to understand such a lengthy query and its purpose:
List<Product> findAllByNameLikeAndCategoryInAndPriceBetweenAndManufacturingPlace_State(String name,
List<Category> categories,
Double bottom, Double top,
STATE state);
With Specifications, we can tackle these issues by creating atomic predicates. And by giving those predicates a meaningful name we can clearly specify their intent. We’ll see how we can convert the above into a much more meaningful query in the section Writing Queries With Specifications section.
Specifications allow us to write queries programmatically. Because of this, we can build queries dynamically based on user input. We’ll see this in more detail in the section Dynamic Queries With Specifications.
Setting Things Up
First, we need to have the Spring Data Jpa dependency in our build.gradle
file:
...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
annotationProcessor 'org.hibernate:hibernate-jpamodelgen'
...
We have also added add the hibernate-jpamodelgen
annotation processor
dependency which will generate static metamodel classes of our entities.
The Generated Metamodel
The classes generated by the Hibernate JPA model generator will allow us to write queries in a strongly-typed manner.
For instance, let’s look at the JPA entity Distributor
:
@Entity
public class Distributor {
@Id
private String id;
private String name;
@OneToOne
private Address address;
//Getter setter ignored for brevity
}
The metamodel class of the Distributor
entity would look like the following:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Distributor.class)
public abstract class Distributor_ {
public static volatile SingularAttribute<Distributor, Address> address;
public static volatile SingularAttribute<Distributor, String> name;
public static volatile SingularAttribute<Distributor, String> id;
public static final String ADDRESS = "address";
public static final String NAME = "name";
public static final String ID = "id";
}
We can now use Distributor_.name
in our criteria queries instead of directly using string field names of our entities.
A major benefit of this is that queries using the metamodel evolve with the entities and are much easier to refactor than string queries.
Writing Queries With Specifications
Let’s convert the findAllByNameLike()
query mentioned above into a Specification
:
List<Product> findAllByNameLike(String name);
An equivalent Specification
of this query method is:
private Specification<Product> nameLike(String name){
return new Specification<Product>() {
@Override
public Predicate toPredicate(Root<Product> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}
};
}
With a Java 8 Lambda we can simplify the above to the following:
private Specification<Product> nameLike(String name){
return (root, query, criteriaBuilder)
-> criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
}
We can also write it in-line at the spot in the code where we need it:
...
Specification<Product> nameLike =
(root, query, criteriaBuilder) ->
criteriaBuilder.like(root.get(Product_.NAME), "%"+name+"%");
...
But this defeats our purpose of reusability, so let’s avoid this unless our use case requires it.
To execute Specifications we need to extend the JpaSpecificationExecutor
interface in our Spring Data JPA repository:
interface ProductRepository extends JpaRepository<Product, String>,
JpaSpecificationExecutor<Product> {
}
The JpaSpecificationExecutor
interface adds methods which will allow us to execute Specification
s, for example, these:
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
Finally, to execute our query we can simply call:
List<Product> products = productRepository.findAll(namelike("reflectoring"));
We can also take advantage of findAll()
functions overloaded with Pageable
and Sort
in case we are expecting a large number of records in the result or
want records in sorted order.
The Specification
interface also has the public static helper methods and()
, or()
, and where()
that allow us to combine
multiple specifications. It also provides a not()
method which allows us to negate a Specification
.
Let’s look at an example:
public List<Product> getPremiumProducts(String name,
List<Category> categories) {
return productRepository.findAll(
where(belongsToCategory(categories))
.and(nameLike(name))
.and(isPremium()));
}
private Specification<Product> belongsToCategory(List<Category> categories){
return (root, query, criteriaBuilder)->
criteriaBuilder.in(root.get(Product_.CATEGORY)).value(categories);
}
private Specification<Product> isPremium() {
return (root, query, criteriaBuilder) ->
criteriaBuilder.and(
criteriaBuilder.equal(
root.get(Product_.MANUFACTURING_PLACE)
.get(Address_.STATE),
STATE.CALIFORNIA),
criteriaBuilder.greaterThanOrEqualTo(
root.get(Product_.PRICE), PREMIUM_PRICE));
}
Here, we have combined belongsToCategory()
, nameLike()
and isPremium()
specifications into one using the where()
and and()
helper functions.
This also reads really nice, don’t you think? Also, notice how isPremium()
is giving more meaning to the query.
Currently, isPremium()
is combining two predicates, but if we want, we can create separate specifications for each of those
and combine again with and()
. For now, we will keep it as is, because the predicates used in isPremium()
are very
specific to that query, and if in the future we need to use them in other queries too then we can always split them up without
impacting the clients of isPremium()
function.
Dynamic Queries With Specifications
Let’s say we want to create an API that allows our clients to fetch all the products and also filter them based on a number of properties such as categories, price, color, etc. Here, we don’t know beforehand what combination of properties the client is going to use to filter the products.
One way to handle this is to write query methods for all possible combinations but that would require writing a lot of query methods. And that number would increase combinatorically as we introduce new fields.
A better solution is to take predicates directly from clients and convert them to database queries using specifications. The client has to simply provide us the list
of Filter
s, and our backend will take care of the rest. Let’s see how we can do this.
First, let’s create an input object to take filters from the clients:
public class Filter {
private String field;
private QueryOperator operator;
private String value;
private List<String> values;//Used in case of IN operator
}
We will expose this object to our clients via a REST API.
Second, we need to write a function that will convert a Filter
to a Specification
:
private Specification<Product> createSpecification(Filter input) {
switch (input.getOperator()){
case EQUALS:
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get(input.getField()),
castToRequiredType(root.get(input.getField()).getJavaType(),
input.getValue()));
case NOT_EQUALS:
return (root, query, criteriaBuilder) ->
criteriaBuilder.notEqual(root.get(input.getField()),
castToRequiredType(root.get(input.getField()).getJavaType(),
input.getValue()));
case GREATER_THAN:
return (root, query, criteriaBuilder) ->
criteriaBuilder.gt(root.get(input.getField()),
(Number) castToRequiredType(
root.get(input.getField()).getJavaType(),
input.getValue()));
case LESS_THAN:
return (root, query, criteriaBuilder) ->
criteriaBuilder.lt(root.get(input.getField()),
(Number) castToRequiredType(
root.get(input.getField()).getJavaType(),
input.getValue()));
case LIKE:
return (root, query, criteriaBuilder) ->
criteriaBuilder.like(root.get(input.getField()),
"%"+input.getValue()+"%");
case IN:
return (root, query, criteriaBuilder) ->
criteriaBuilder.in(root.get(input.getField()))
.value(castToRequiredType(
root.get(input.getField()).getJavaType(),
input.getValues()));
default:
throw new RuntimeException("Operation not supported yet");
}
}
Here we have supported several operations such as EQUALS
, LESS_THAN
, IN
, etc. We can also
add more based on our requirements.
Now, as we know, the Criteria API allows us to write typesafe queries. So, the values that we provide must be of the type compatible with the type of our field. Filter
takes the value as String
which means we will have to cast the values to a required type
before passing it to CriteriaBuilder
:
private Object castToRequiredType(Class fieldType, String value) {
if(fieldType.isAssignableFrom(Double.class)) {
return Double.valueOf(value);
} else if(fieldType.isAssignableFrom(Integer.class)) {
return Integer.valueOf(value);
} else if(Enum.class.isAssignableFrom(fieldType)) {
return Enum.valueOf(fieldType, value);
}
return null;
}
private Object castToRequiredType(Class fieldType, List<String> value) {
List<Object> lists = new ArrayList<>();
for (String s : value) {
lists.add(castToRequiredType(fieldType, s));
}
return lists;
}
Finally, we add a function that will combine multiple Filters to a specification:
private Specification<Product> getSpecificationFromFilters(List<Filter> filter){
Specification<Product> specification =
where(createSpecification(queryInput.remove(0)));
for (Filter input : filter) {
specification = specification.and(createSpecification(input));
}
return specification;
}
Now, let’s try to fetch all the products belonging to the MOBILE
or TV APPLIANCE
category and whose prices are below 1000 using
our new shiny dynamic specifications query generator.
Filter categories = Filter.builder()
.field("category")
.operator(QueryOperator.IN)
.values(List.of(Category.MOBILE.name(),
Category.TV_APPLIANCES.name()))
.build();
Filter lowRange = Filter.builder()
.field("price")
.operator(QueryOperator.LESS_THAN)
.value("1000")
.build();
List<Filter> filters = new ArrayList<>();
filters.add(lowRange);
filters.add(categories);
productRepository.getQueryResult(filters);
The above code snippets should do for most filter cases but there is still a lot of room for improvement. Such as allowing queries
based on nested entity properties (manufacturingPlace.state
) or limiting the fields on which we want to allow filters.
Consider this as an open-ended problem.
When Should I Use Specifications Over Query Methods?
One question that comes to mind is that if we can write any query with specifications then when do we prefer query methods? Or should we ever prefer them? I believe there are a couple of cases where query methods could come in handy.
Let’s say our entity has only a handful of fields, and it only needs to be queried in a certain way then why bother writing Specifications when we can simply write a query method?
And if future requirements come in for more queries for the given entity then we can always refactor it to use Specifications. Also, Specifications won’t be helpful in cases where we want to use database-specific features in a query, for example performing JSON queries with PostgresSQL.
Conclusion
Specifications provide us with a way to write reusable queries and also fluent APIs with which we can combine and build more sophisticated queries.
All in all, Spring JPA Specifications is a great tool whether we want to create reusable predicates or want to generate typesafe queries programmatically.
Thank you for reading! You can find the working code at GitHub.