Bubu Tripathy
·
Follow
5 min read
·
May 12, 2023
--
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:
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.
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
.
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
.
Root
object represents the entity being queried and allows access to its attributes.CriteriaQuery
object defines the query structure and can be used to modify the query aspects like ordering and grouping.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.
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.
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.
For more information, please visit chinese electrical valve actuators, advantages of a pneumatic system, electric choke valve actuators supplier.