What is Spring data specification?

15 Apr.,2024

 

Dynamic Query with Specification Interface in Spring Data JPA

Bubu Tripathy

·

Follow

5 min read

·

May 12, 2023

--

Introduction

The Specification interface in Spring Data JPA is a powerful tool that allows developers to build dynamic queries with criteria-based predicates. It provides a flexible and expressive way to construct complex queries at runtime, enabling you to create more advanced and customizable data retrieval operations.

With the Specification interface, you can define sets of criteria that specify the conditions your query should satisfy. These criteria can be combined using logical operators like AND and OR, allowing you to create intricate query logic tailored to your application’s requirements. By using the Specification interface, you can avoid writing multiple query methods for different combinations of search criteria, resulting in cleaner and more maintainable code.

In this tutorial, we’ll explore how to use the Specification interface in Spring Data JPA. Here’s a step-by-step guide on how to use the Specification interface in Spring Data JPA:

Set up Project

Make sure you have a Spring Boot project set up with Spring Data JPA. You’ll need the necessary dependencies and configurations in your project’s pom.xml or build.gradle file.

Define Entity

Create your JPA entity class that represents the database table you want to query. For example, let’s assume we have an entity called Product with properties like id, name, and price.

Create a Specification Class

Once you have set up your project and defined your JPA entity, the next step is to create a separate class that implements the Specification interface. This class will serve as a container for defining the dynamic query predicates using criteria-based conditions.

The Specification interface is part of the org.springframework.data.jpa.domain package and provides a single method, toPredicate, which is responsible for constructing the criteria-based predicates. The toPredicate method takes three parameters: Root, CriteriaQuery, and CriteriaBuilder.

  • The Root object represents the entity being queried and allows access to its attributes.
  • The CriteriaQuery object defines the query structure and can be used to modify the query aspects like ordering and grouping.
  • The CriteriaBuilder object provides a set of methods for building the criteria-based predicates.

Let’s illustrate the process by creating a sample Specification class called ProductSpecifications for the Product entity.

import org.springframework.data.jpa.domain.Specification;

public class ProductSpecifications {

public static Specification<Product> hasName(String name) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("name"), name);
}

public static Specification<Product> hasPriceGreaterThan(double price) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.greaterThan(root.get("price"), price);
}

// Add more specifications as needed
}

In the ProductSpecifications class, we define two example specifications: hasName and hasPriceGreaterThan. Each specification method returns a Specification instance that encapsulates the criteria-based predicate logic.

In the hasName method, we use the criteriaBuilder.equal method to create a predicate that checks if the value of the "name" attribute in the entity matches the provided name parameter.

Similarly, in the hasPriceGreaterThan method, we utilize the criteriaBuilder.greaterThan method to construct a predicate that verifies if the "price" attribute in the entity is greater than the given price parameter.

Let’s see a second example to solidify our understanding.

import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;

public class OrderSpecifications {

public static Specification<Order> hasCustomerName(String name) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("customer").get("name"), name);
}

public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("status"), status);
}

public static Specification<Order> hasProductInCategory(String category) {
return (root, query, criteriaBuilder) -> {
Join<Order, Product> productJoin = root.join("products", JoinType.INNER);
return criteriaBuilder.equal(productJoin.get("category"), category);
};
}

public static Specification<Order> hasProductPriceGreaterThan(double price) {
return (root, query, criteriaBuilder) -> {
Join<Order, Product> productJoin = root.join("products", JoinType.INNER);
return criteriaBuilder.greaterThan(productJoin.get("price"), price);
};
}

// Add more specifications as needed
}

In this example, we assume we have an Order entity that has a customer association and a products association representing a one-to-many relationship with the Product entity.

The hasCustomerName specification checks if the order's customer name matches the provided name parameter. It accesses the name attribute of the customer association using the root.get("customer").get("name") expression.

The hasStatus specification verifies if the order's status matches the provided OrderStatus parameter. It directly compares the status attribute of the order entity.

The hasProductInCategory specification checks if any of the order's products belong to the specified category. It performs an inner join with the products association and compares the category attribute of the joined Product entity.

The hasProductPriceGreaterThan specification ensures that at least one product in the order has a price greater than the provided price parameter. It also performs an inner join with the products association and compares the price attribute of the joined Product entity.

By encapsulating the query predicates within the Specification class, you can achieve a modular and reusable approach to defining dynamic queries. The Specification class allows you to separate the query logic from the repository, promoting better code organization and maintainability.

Once you have defined the Specification class, you can proceed to utilize these specifications in your repository to execute dynamic queries.

Here’s an example of how you can use the OrderSpecifications class in a repository to execute dynamic queries based on the defined specifications:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

private final OrderRepository orderRepository;

@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

public Page<Order> searchOrders(String customerName, OrderStatus status, String productCategory, double minProductPrice, Pageable pageable) {
Specification<Order> spec = Specification.where(null);

if (customerName != null && !customerName.isEmpty()) {
spec = spec.and(OrderSpecifications.hasCustomerName(customerName));
}

if (status != null) {
spec = spec.and(OrderSpecifications.hasStatus(status));
}

if (productCategory != null && !productCategory.isEmpty()) {
spec = spec.and(OrderSpecifications.hasProductInCategory(productCategory));
}

if (minProductPrice > 0) {
spec = spec.and(OrderSpecifications.hasProductPriceGreaterThan(minProductPrice));
}

return orderRepository.findAll(spec, pageable);
}
}

In this example, the OrderService class demonstrates how to use the OrderSpecifications class to perform dynamic queries on the Order entity.

The searchOrders method takes in various search parameters such as customerName, status, productCategory, minProductPrice, and pageable. It constructs the dynamic query specification based on the provided parameters.

The initial specification is set to Specification.where(null) to start with a neutral predicate. Then, if the corresponding search parameters are provided, the relevant specifications from the OrderSpecifications class are appended to the existing specification using the and method. This allows the specifications to be combined using logical AND conditions.

Finally, the orderRepository.findAll(spec, pageable) method is called to execute the dynamic query using the constructed specification and retrieve the results in a paginated manner.

Conclusion

By implementing the Specification interface, you can define a separate class to encapsulate the criteria-based predicates. This approach promotes code modularity, reusability, and maintainability. The Specification class allows you to define various specifications, each representing a specific condition or combination of conditions for your dynamic queries.

Specifications

JPA 2 introduces a criteria API that you can use to build queries programmatically. By writing a criteria, you define the where clause of a query for a domain class. Taking another step back, these criteria can be regarded as a predicate over the entity that is described by the JPA criteria API constraints.

Spring Data JPA takes the concept of a specification from Eric Evans' book, “Domain Driven Design”, following the same semantics and providing an API to define such specifications with the JPA criteria API. To support specifications, you can extend your repository interface with the JpaSpecificationExecutor interface, as follows:

public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
 …
}

The additional interface has methods that let you run specifications in a variety of ways. For example, the findAll method returns all entities that match the specification, as shown in the following example:

List<T> findAll(Specification<T> spec);

The Specification interface is defined as follows:

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}

Specifications can easily be used to build an extensible set of predicates on top of an entity that then can be combined and used with JpaRepository without the need to declare a query (method) for every needed combination, as shown in the following example:

Example 1. Specifications for a Customer

public class CustomerSpecs {


  public static Specification<Customer> isLongTermCustomer() {
    return (root, query, builder) -> {
      LocalDate date = LocalDate.now().minusYears(2);
      return builder.lessThan(root.get(Customer_.createdAt), date);
    };
  }

  public static Specification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {
    return (root, query, builder) -> {
      // build query here
    };
  }
}

The Customer_ type is a metamodel type generated using the JPA Metamodel generator (see the Hibernate implementation’s documentation for an example). So the expression, Customer_.createdAt, assumes the Customer has a createdAt attribute of type Date. Besides that, we have expressed some criteria on a business requirement abstraction level and created executable Specifications. So a client might use a Specification as follows:

Example 2. Using a simple Specification

List<Customer> customers = customerRepository.findAll(isLongTermCustomer());

Why not create a query for this kind of data access? Using a single Specification does not gain a lot of benefit over a plain query declaration. The power of specifications really shines when you combine them to create new Specification objects. You can achieve this through the default methods of Specification we provide to build expressions similar to the following:

Example 3. Combined Specifications

MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
  isLongTermCustomer().or(hasSalesOfMoreThan(amount)));

Specification offers some “glue-code” default methods to chain and combine Specification instances. These methods let you extend your data access layer by creating new Specification implementations and combining them with already existing implementations.

And with JPA 2.1, the CriteriaBuilder API introduced CriteriaDelete. This is provided through JpaSpecificationExecutor’s `delete(Specification) API.

Example 4. Using a Specification to delete entries.

Specification<User> ageLessThan18 = (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), 18)

userRepository.delete(ageLessThan18);

The Specification builds up a criteria where the age field (cast as an integer) is less than 18. Passed on to the userRepository, it will use JPA’s CriteriaDelete feature to generate the right DELETE operation. It then returns the number of entities deleted.

What is Spring data specification?

Specifications :: Spring Data JPA

For more information, please visit chinese electrical valve actuators, advantages of a pneumatic system, electric choke valve actuators supplier.