Data JPA and JpaSpecificationExecutor
(Spring Boot)
Introduction
Flexible and clean searching solution in Spring Boot.
Reference
Spring Data JPA - Using Specifications to execute JPA Criteria Queries
https://www.logicbig.com/tutorials/spring-framework/spring-data/specifications.html
Getting Started with Spring Data Specifications
https://reflectoring.io/spring-data-specifications/
JPA Static Metamodel Generator
Interface CriteriaBuilder
Specification interface
Spring Data provides Specification interface which can be used to execute JPA criteria queries:
package org.springframework.data.jpa.domain;
....
public interface Specification<T> extends Serializable {
//Negates the given spec
static <T> Specification<T> not(Specification<T> spec) {
return Specifications.negated(spec);
}
//Applies Where clause the give spec
static <T> Specification<T> where(Specification<T> spec) {
return Specifications.where(spec);
}
//Applies AND condition to the give spec
default Specification<T> and(Specification<T> other) {
return Specifications.composed(this, other, AND);
}
//Applies OR condition to the give spec
default Specification<T> or(Specification<T> other) {
return Specifications.composed(this, other, OR);
}
//Creates a Predicate
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder);
}
There's only one abstract method 'toPredicate()' which returns javax.persistence.criteria.Predicate. We only have to implement 'toPredicate()' method and return the Where Clause predicate, the rest of the stuff i.e. creating CriteriaBuilder, Root etc is automatically provided to us by Spring.
All other methods are helper methods to create composite Specifications.
JpaSpecificationExecutor interface
To use Specifications we also have to extend our repository interface with JpaSpecificationExecutor interface. This interface provides methods to execute Specifications. Here's this interface snippet:
package org.springframework.data.jpa.repository;
......
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(@Nullable Specification<T> spec);
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
long count(@Nullable Specification<T> spec);
}
pom.xml
Hibernate Static Metamodel Generator is an annotation processor based on JSR_269 with the task of creating JPA 2 static metamodel classes. Eg: For the entity class Employee, it creates the abstract class Employee_.
As detailed in the reference 'JPA Static Metamodel Generator', one way of configuring the generation of the metamodel is configuring the annotationProcessorPath of the maven-compiler-plugin, eg:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<-- jpamodelgen not available w/ sb3 (hibernate 6.1.5) -->
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
</annotationProcessorPaths>
<encoding>${project.build.sourceEncoding}</encoding>
<parameters>true</parameters>
</configuration>
<dependencies>
<!-- Java bytecode manipulation framework -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.1</version>
</dependency>
</dependencies>
</plugin>
Tips
Path of the attribute of a nested object
javax.persistence.criteria.Predicate filter1 = criteriaBuilder.equal(root.get(MyEntity_.id).get(MyEntityId_.attrA), objectA);
Sample entities
@Entity
public class Employee {
@Id
@GeneratedValue
private long id;
private String nickname;
private String firstName;
private String lastName;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Phone> phones;
}
@Entity
public class Phone {
@Id
@GeneratedValue
private long id;
private PhoneType type;
private String number;
}
public enum PhoneType {
Home,
Cell,
Work
}
Sample repository
It extends JpaSpecificationExecutor:
public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
}
Sample specifications
public final class EmployeeSpecs {
//Employees by nickname
public static Specification<Employee> getEmployeesByNicknameSpec(String nickname) {
return (root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get(Employee_.nickname), nickname);
};
}
//Employees by firstName
public static Specification<Employee> byFirstNameSpec(String firstName) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.equal(root.get(Employee_.firstName), firstName);
}
//Employees by lastName
public static Specification<Employee> byLastNameSpec(String lastName) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.equal(root.get(Employee_.lastName), lastName);
}
//Employees by phone type (join)
public static Specification<Employee> getEmployeesByPhoneTypeSpec(PhoneType phoneType) {
return (root, query, criteriaBuilder) -> {
ListJoin<Employee, Phone> phoneJoin = root.join(Employee_.phones);
return criteriaBuilder.equal(phoneJoin.get(Phone_.type), phoneType);
};
}
//Employees by first or last name (would be possible to chain the ".equal" with ".and" or ".notNull")
public static Specification<Employee> getEmployeesByFirstNameOrLastName(String firstName, String lastName) {
return (root, query, criteriaBuilder) -> {
return criteriaBuilder.or(
criteriaBuilder.equal(root.get(Employee_.firstName), firstName),
criteriaBuilder.equal(root.get(Employee_.lastName), lastName)
);
};
}
//Employees by nickname like %nick%
public static Specification<Employee> getEmployeesByNicknameLike(String nick) {
return (root, query, criteriaBuilder) -> return criteriaBuilder.like(root.get(Employee_.nickname), "%"+nick+"%");
}
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));
}
}
Sample service
@Service
public class ExampleService {
@Inject
private EmployeeRepository repo;
public List<Employee> findEmployeesByName() {
System.out.println("-- finding employees with nickname Tim --");
//calling JpaSpecificationExecutor#findAll(Specification)
List<Employee> list = repo.findAll(EmployeeSpecs.getEmployeesByNicknameSpec("Tim"));
return list;
}
public List<Employee> findEmployeesByPhoneType() {
System.out.println("-- finding employees by phone type Cell --");
//calling JpaSpecificationExecutor#findAll(Specification)
List<Employee> list = repo.findAll(EmployeeSpecs.getEmployeesByPhoneTypeSpec(PhoneType.Cell));
return list;
}
//where() allows to combine multiple specifications
public List<Product> getPremiumProducts(String name, List<Category> categories) {
return productRepository.findAll( //
where(belongsToCategory(categories)) //
.and(nameLike(name)) //
.and(isPremium()) //
);
}
/* DOC. null values are filtered out, so if we return null instead of our actual
* specification, we can properly filter these depending on the input values. This
* means that if lastName is null, we won’t filter by byLastNameSpec().
*/
public List<Employee> getByOptCriteria(String fn, @Nullable String ln) {
Specification<Employee> spec = Specification //
.where(EmployeeSpecs.byFirstNameSpec(fn))//
.and(lastName== null ? null
: EmployeeSpecs.byLastNameSpec(ln))//
;
return employeeRepo.findAll(spec);
}
}