JPA Specification

About

JPA Specification is a part of Spring Data JPA and is used to construct dynamic queries in a more flexible and type-safe manner. It allows for building complex queries with conditions like filtering, sorting, and pagination without needing to write JPQL (Java Persistence Query Language) directly.

Specifications are typically used with Spring Data JPA repositories and can be combined with other specifications for more complex queries.

In a Spring Boot application, using JPA Specifications allows us to create dynamic queries based on the filtering and sorting criteria. The JPA Criteria API can be complex for simple use cases, so JPA Specifications provide a cleaner and more flexible way to implement dynamic queries.

Components of JPA Specification

  1. Specification Interface:

    • A Specification interface defines a criterion (condition) that can be used to filter results from the database. It works with the CriteriaBuilder and CriteriaQuery of JPA.

  2. Specification API:

    • The Specification API allows for building complex queries dynamically using method chaining.

Why Use JPA Specifications?

  • Dynamic Queries: Allows building dynamic queries based on user input without writing custom JPQL or SQL.

  • Type-Safe: As it is based on Java methods, it is type-safe and prevents errors like incorrect field names or mismatched types.

  • Composability: Specifications can be combined together to form more complex queries.

  • Maintainable: It’s easier to maintain and extend over writing manual JPQL queries.

Creating Specifications

In Spring Data JPA, Specifications are implemented as classes that implement the Specification<T> interface.

Example 1: Basic Specification

Define Specification

import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;

public class EmployeeSpecification implements Specification<Employee> {

    private String name;

    public EmployeeSpecification(String name) {
        this.name = name;
    }

    @Override
    public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        if (name == null || name.isEmpty()) {
            return criteriaBuilder.conjunction(); // No filter if name is null or empty
        }
        return criteriaBuilder.like(root.get("name"), "%" + name + "%");
    }
}

Usage in Repository

We can use Specification directly in our repository to query data.

package com.example.demo.repository;

import com.example.demo.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
}

Then, we can call it like this:

@Autowired
private EmployeeRepository employeeRepository;

public List<Employee> searchEmployees(String name) {
    EmployeeSpecification spec = new EmployeeSpecification(name);
    return employeeRepository.findAll(spec);
}

Example 2: Combining Specifications

We can combine multiple specifications using the Specification.where() and and()/or() methods.

Create Multiple Specifications

public class EmployeeSpecifications {

    public static Specification<Employee> hasName(String name) {
        return (root, query, builder) -> {
            if (name != null) {
                return builder.like(root.get("name"), "%" + name + "%");
            }
            return builder.conjunction();
        };
    }

    public static Specification<Employee> hasDepartment(String department) {
        return (root, query, builder) -> {
            if (department != null) {
                return builder.equal(root.get("department"), department);
            }
            return builder.conjunction();
        };
    }

    public static Specification<Employee> hasSalaryGreaterThan(Double salary) {
        return (root, query, builder) -> {
            if (salary != null) {
                return builder.greaterThan(root.get("salary"), salary);
            }
            return builder.conjunction();
        };
    }
}

Combining Specifications

Specification<Employee> spec = Specification.where(EmployeeSpecifications.hasName("John"))
        .and(EmployeeSpecifications.hasDepartment("Engineering"))
        .and(EmployeeSpecifications.hasSalaryGreaterThan(50000.0));

List<Employee> employees = employeeRepository.findAll(spec);

Example 3: Specification with Pagination

We can also add pagination along with specifications to return paginated results.

public Page<Employee> searchEmployeesWithPagination(String name, String department, Double salary, Pageable pageable) {
    Specification<Employee> spec = Specification.where(EmployeeSpecifications.hasName(name))
        .and(EmployeeSpecifications.hasDepartment(department))
        .and(EmployeeSpecifications.hasSalaryGreaterThan(salary));

    return employeeRepository.findAll(spec, pageable);
}

Example 4: JPA Specification with Sorting

We can add sorting criteria along with specifications:

public Page<Employee> searchEmployeesWithPaginationAndSorting(String name, String department, Double salary, Pageable pageable) {
    Specification<Employee> spec = Specification.where(EmployeeSpecifications.hasName(name))
        .and(EmployeeSpecifications.hasDepartment(department))
        .and(EmployeeSpecifications.hasSalaryGreaterThan(salary));

    return employeeRepository.findAll(spec, pageable);
}

Here, the Pageable object not only handles pagination but also applies sorting as per the user’s request.

Last updated

Was this helpful?