The query methods of Spring Data JPA are a convenient way to retrieve data from your database. By simply defining methods in an interface, the framework can derive queries. For more complicated things, you can also define named queries and write your own JPQL or native SQL queries. However, with a growing application, this approach shows its drawbacks. New use-cases require new, but only slightly different, queries. The results are growing repositories that become harder and harder to maintain.
In this blog post, I’ll show you how to use Spring Data’s Specification
to address this problem.
The Example
We will extend the example code of my previous blog post ‘How to test the Data Layer of your Spring Boot Application with @DataJpaTest’. There we implemented a named query method to retrieve all restaurants with a rating of at least 8/10.
public interface RestaurantRepository extends JpaRepository<Restaurant, Long> {
@Query(value = "SELECT r FROM Restaurant r LEFT JOIN r.ratings ra GROUP BY r having avg(ra.score) >= 8")
List<Restaurant> findTopRatedRestaurants();
}
- We used a JPQL statement to retrieve the top-rated restaurants.
What’s the problem with this method? If the code remains as it is right now, nothing. As written in the intro, the problems with query methods - named and derived alike - start to show as soon as our application grows. Let’s say we want to implement the option to retrieve top-rated restaurants while also filtering their name. We would need to implement a new method:
public interface RestaurantRepository extends JpaRepository<Restaurant, Long> {
@Query(value = "SELECT r FROM Restaurant r LEFT JOIN r.ratings ra GROUP BY r having avg(ra.score) >= 8")
List<Restaurant> findTopRatedRestaurants();
@Query(value = "SELECT r FROM Restaurant r LEFT JOIN r.ratings ra "
+ "where r.name like %:name% GROUP BY r having avg(ra.score) >= 8")
List<Restaurant> findTopRatedRestaurantsByName(@Param("name") String name);
}
- We duplicated the complete method, just to extend it with one parameter.
Imagine we want to filter even more attributes of a restaurant. We would start to implement nearly the same thing over and over again. This would become a maintenance nightmare. If the name of one entity or property would change, we would need to edit every single query method. A manual task that is quite error-prone.
Thankfully, Spring Data supports different ways to retrieve data. In situations like these, Specification
s can improve your code significantly.
What is a Specification
The idea of Spring Data’s Specification
originates in the Specification pattern. The pattern was developed by Eric Evans and was later refined together with Martin Fowler. The reasoning behind the pattern is the separation of the way an object is matched from the object itself.
In our example, this means we won’t implement an isTopRatedAndHasNameLike
method in the Restaurant
class itself but put the predicates that must match into a class of its own - a specification. By doing this, we create a clear separation of responsibilities.
The original paper mentions three implementation strategies for the pattern. Spring uses the “composite specification”, which is built on top of the GoF’s Composite pattern. Instead of building one really specific specification, we implement multiple ones, that can be freely combined in any way.
Following the pattern, Spring Data’s Specification
is an abstraction on top of JPAs Criteria API that allows the building of atomic predicates, which can be freely combined.
Implementation
The first thing we’ll do is to add org.hibernate:hibernate-jpamodelgen
as a new dependency to the project. Although this is optional, I suggest doing it to make the code of our specifications type-safe.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
</dependency>
In the next step, we extend our repository to add support for Specification
s:
public interface RestaurantRepository extends JpaRepository<Restaurant, Long>,
JpaSpecificationExecutor<Restaurant> {
// old query methods omitted for brevity
}
- The repository also extends the
JpaSpecificationExecutor
interface.
Now we can implement the Specification itself:
class RestaurantTopRatedSpecification implements Specification<Restaurant> {
public static final double MINIMAL_AVERAGE_RATING = 8.0;
@Override
public Predicate toPredicate(Root<Restaurant> restaurant,
CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
final var averageRating = criteriaBuilder.avg(restaurant.join(Restaurant_.RATINGS).get(Rating_.SCORE));
final var topRatedPredicate = criteriaBuilder.greaterThanOrEqualTo(averageRating, MINIMAL_AVERAGE_RATING);
query.groupBy(restaurant.get(Restaurant_.ID)).having(topRatedPredicate);
return null;
}
}
- Our specification implements the
Specification
interface. - The
toPredicate
method contains the logic of the specification. - The
Restaurant_
class was generated byhibernate-jpamodelgen
. Instead of using a plain String, we use constants that will change if we rename a property of our entity. This will lead to errors at compile- and not at runtime. - We use the
CriteriaBuilder
and theCriteriaQuery
to build our condition. - Normally, we would return a predicate. This specification only adds a “groupBy” and “having” and therefore does not return a predicate.
As you may have noticed, this specification is package private. This is not needed in any way, it’s just my personal preference. I like to provide all specifications through a facade. This way I can provide instances of the specifications through static methods, which helps to improve the readability of the clients that use them.
public final class RestaurantSpecifications {
private RestaurantSpecifications() {
}
public static Specification<Restaurant> hasTopRating() {
return new RestaurantTopRatedSpecification();
}
}
- A simple static method that returns an instance of the specification we just created.
Now we can create a service that uses the new specification to retrieve some data:
@Service
public class RestaurantService {
private final RestaurantRepository restaurantRepository;
// constructor ommited for brevity
public List<Restaurant> findTopRated() {
return restaurantRepository.findAll(where(hasTopRating()));
}
}
- We use the
findAll
method of our repository with our newly created specification. The staticwhere
method is provided by theSpecification
interface. Its sole purpose is to make the method call read a little more like an English sentence. Our statichasTopRating
method complements this idea.
Combine multiple Specifications
As written earlier, the problems with query methods emerge when you need to combine multiple predicates. To properly demonstrate the advantages of specifications over query methods, we need to implement a second one, that can be combined with the RestaurantTopRatedSpecification
:
class RestaurantNameSpecification implements Specification<Restaurant> {
private final String name;
public RestaurantNameSpecification(String name) {
this.name = name;
}
@Override
public Predicate toPredicate(Root<Restaurant> restaurant, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
final var nameExpression = criteriaBuilder.lower(restaurant.get(Restaurant_.name));
return criteriaBuilder.like(nameExpression, "%" + name.toLowerCase() + "%");
}
}
- This specification abstracts a predicate of the restaurant’s name. Therefore, we pass a name to filter for into it.
- We use the
CriteriaBuilder
to build the predicate.
Now we can combine both of our specifications:
@Service
public class RestaurantService {
//other code omitted for brevity
public List<Restaurant> findTopRatedByName(String name) {
return restaurantRepository.findAll(where(nameIsLike(name)).and(hasTopRating()));
}
}
- We use the
and
method of theSpecification
interface, to combine both of our specifications. The interface also provides anor
andnot
method.
Testing a Specification
The implementation of the specifications may be done, but we still need to test them. As mentioned before, a Specification
is an abstraction that makes usage of the JPA Criteria API. Therefore, we can use Spring’s @DataJpaTest
to test them:
@DataJpaTest
class RestaurantNameSpecificationTest {
@Autowired
private RestaurantRepository restaurantRepository;
@BeforeEach
void setup() {
restaurantRepository.save(new Restaurant("Café Java"));
restaurantRepository.save(new Restaurant("Spring Restaurant"));
restaurantRepository.save(new Restaurant("Jakarta Restaurant"));
}
@Test
void matchesName() {
final var topRestaurants = restaurantRepository.findAll(
where(nameIsLike("Restaurant"))
);
assertThat(topRestaurants, hasSize(2));
assertThat(topRestaurants, contains(
hasProperty("name", is("Spring Restaurant")),
hasProperty("name", is("Jakarta Restaurant"))
));
}
}
- We use the
@DataJpaTest
test slice, to test our specification. - This setup method populates the database with our test data.
- We use our specification to query for data in the database.
- Hamcrest offers some neat options to make assertions about collections and their content.
Conclusion
Specifications can be a solution to the problem of an ever-growing repository. By implementing atomic specifications, that can be combined in any way, we can drastically reduce the number of query methods and duplicated code. But as with almost all choices, you should think about when to use them. Although specifications are a versatile tool that can be used in any situation, I still would advise you to use them only when needed. They won’t come for free. You and your team need to understand the concept of the Specification and Composite pattern, you need to understand the JPA Criteria API and you need to test and maintain them. For all those reasons, I still use the derived named queries wherever it is reasonable. They are easy to understand, write, and - to a certain degree - easy to maintain.
Resources
- You can find the code of this blog post in this GitHub repository
- For more details about the testing with
@DataJpaTest
have a look at my blog post ‘How to test the Data Layer of your Spring Boot Application with @DataJpaTest’ - In my follow-up blog post, I show you how to test the composition of specifications: ‘How to test the composition of Spring Data Specifications’
- You can find the original paper of the Specification Pattern here: Specifications
- Have a look at Spring Boot’s documentation for more official information about
Specification