Specification & Predicate: Advance Search and Filtering in JPA

In this tutorial, you will learn how to use specification and predicate in Spring Data JPA using the Spring Boot RESTful API project.

Spring Data JPA Specifications allow us to create dynamic database queries by using the JPA Criteria API. It defines a specification as a predicate over an entity.

Spring has a wrapper around the JPA criteria API (that uses predicates) and is called the specification API.

Spring Data JPA repository abstraction allows executing predicates via JPA Criteria API predicates wrapped into a Specification object. To enable this functionality, you simply let your repository extend JpaSpecificationExecutor.

In the below example I’ll explain step by step how to implement specification and predicate in the RESTful API project.

I’ve created a database with the name jp_database. Inside this database, I’ve created one table with the name jp_users.

MySQL database and a Table

Creating an Application

Let us start with creating a simple Spring Boot application with the Spring Data JPA and MySQL database.

I’ve created 3 maven module-based projects with the below project hierarchy:

  • Iats-jp-api
  • Iats-jp-core
  • Iats-jp-parent

Inside the API project, I’ve created a controller class with all the methods like GET, POST, PUT DELETE etc. The core project has all the business logic and the parent project has both modules API and core.

Our application has just one User entity class, as shown below:

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.annotations.Formula;

@Entity
@Table(name = "jp_users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "user_id")
    private Integer userId;

    @Column(name = "email")
    private String email;

    @Column(name = "password")
    private String password;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "middle_name")
    private String middleName;

    @Column(name = "last_name")
    private String lastName;

    @Formula("concat(first_name, middle_name, last_name)")
    private String fullName;

    @Column(name = "gender")
    private String gender;

    @Column(name = "birth_date")
    @Temporal(TemporalType.DATE)
    private Date birthDate;

    @Column(name = "phone_number")
    private String phNumber;

    @Column(name = "summary")
    private String summary;

    @Column(name = "experience")
    private Integer experience;

    @Column(name = "profile_img")
    private String profImg;

    @Column(name = "current_salary")
    private double currSalary;

    @Column(name = "expected_salary")
    private double exptSalary;

    @Column(name = "is_enable")
    private Integer isEnalbe;

    @Column(name = "role_id")
    private Integer roleId;

    @Column(name = "company_id")
    private Integer compId;

    @Column(name = "is_owner")
    private Integer isOwner;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getMiddleName() {
        return middleName;
    }

    public void setMiddleName(String middleName) {
        this.middleName = middleName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Date getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }

    public String getPhNumber() {
        return phNumber;
    }

    public void setPhNumber(String phNumber) {
        this.phNumber = phNumber;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }

    public Integer getExperience() {
        return experience;
    }

    public void setExperience(Integer experience) {
        this.experience = experience;
    }

    public String getProfImg() {
        return profImg;
    }

    public void setProfImg(String profImg) {
        this.profImg = profImg;
    }

    public double getCurrSalary() {
        return currSalary;
    }

    public void setCurrSalary(double currSalary) {
        this.currSalary = currSalary;
    }

    public double getExptSalary() {
        return exptSalary;
    }

    public void setExptSalary(double exptSalary) {
        this.exptSalary = exptSalary;
    }

    public Integer getIsEnalbe() {
        return isEnalbe;
    }

    public void setIsEnalbe(Integer isEnalbe) {
        this.isEnalbe = isEnalbe;
    }

    public Integer getRoleId() {
        return roleId;
    }

    public void setRoleId(Integer roleId) {
        this.roleId = roleId;
    }

    public Integer getCompId() {
        return compId;
    }

    public void setCompId(Integer compId) {
        this.compId = compId;
    }

    public Integer getIsOwner() {
        return isOwner;
    }

    public void setIsOwner(Integer isOwner) {
        this.isOwner = isOwner;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

}

The next step is to create a repository interface called UserRepository to retrieve data from the database. To use Specifications, we also need to extend our repository interface from the JpaSpecificationExecutor interface. This interface provides methods to execute Specifications. Here is how our repository interface looks like:

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import com.iats.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {

    public Page<User> findAll(Specification<User> spec, Pageable pageable);

    public List<User> findAll(Specification<User> spec);

}

Creating Specifications

Let us now start with the most interesting part of this article — creating specifications to execute dynamic queries for searching users in the database.

Create a UserSpecification class and provides an implementation for the getUsers method:

import java.util.ArrayList;
import java.util.List;

import javax.persistence.criteria.Predicate;

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

import com.iats.entity.User;
import com.iats.model.request.UserRequest;

@Component
public class UserSpecification {

    public Specification<User> getUsers(UserRequest request) {
        return (root, query, criteriaBuilder) -> {

            List<Predicate> predicates = new ArrayList<>();

            if (request.getEmail() != null && !request.getEmail().isEmpty()) {
                predicates.add(criteriaBuilder.equal(root.get("email"), request.getEmail()));
            }
            if (request.getName() != null && !request.getName().isEmpty()) {
                predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("fullName")),
                        "%" + request.getName().toLowerCase() + "%"));
            }
            if (request.getGender() != null && !request.getGender().isEmpty()) {
                predicates.add(criteriaBuilder.equal(root.get("gender"), request.getGender()));
            }

            query.orderBy(criteriaBuilder.desc(root.get("experience")));

            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));

        };
    }
}

The UserSpecification class allows you to combine multiple specifications to filter the users using multiple constraints. Using this class, you can easily generate different kinds of database queries dynamically.

In the UserSpecification class, users are filtering based on email, name (which is full name combination of first, middle and last name), and gender using specification and predicate.

UserService Interface and UserServiceImpl Class Implementation

UserService Interface

import com.iats.model.request.UserRequest;
import com.iats.model.response.UserResponseList;

public interface UserService {

    public UserResponseList getUserList(UserRequest userRequest);
}

UserServiceImpl Class

import java.util.ArrayList;
import java.util.List;

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

import com.iats.entity.User;
import com.iats.model.request.UserRequest;
import com.iats.model.response.UserResponseDto;
import com.iats.model.response.UserResponseList;
import com.iats.repository.UserRepository;
import com.iats.service.UserService;
import com.iats.specification.UserSpecification;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserSpecification userSpecification;

    @Value("${pagination.page.size.default}")
    private Integer defaultPageSize;

    @Override
    public UserResponseList getUserList(UserRequest request) {
        List<User> list = null;

        Page<User> pages = null;
        if (request.getPageNumber() == null) {
            pages = new PageImpl<>(userRepository.findAll(userSpecification.getUsers(request)));
        } else {
            if (request.getPageSize() == null) {
                request.setPageSize(defaultPageSize);
            }
            Pageable paging = PageRequest.of(request.getPageNumber() - 1, request.getPageSize());
            pages = userRepository.findAll(userSpecification.getUsers(request), paging);
        }
        if (pages != null && pages.getContent() != null) {
            list = pages.getContent();
            if (list != null && list.size() > 0) {
                UserResponseList respList = new UserResponseList();
                respList.setTotalPages(pages.getTotalPages());
                respList.setTotalCount(pages.getTotalElements());
                respList.setPageNo(pages.getNumber() + 1);
                respList.setUsers(new ArrayList<UserResponseDto>());
                for (User users : list) {
                    UserResponseDto obj = new UserResponseDto();
                    obj.populateObject(users);
                    respList.getUsers().add(obj);
                }
                return respList;
            }
        }
        return null;
    }

}

User Controller Class which is calling getUserList method inside UserServiceImpl class

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.iats.constant.Constant;
import com.iats.model.request.UserRequest;
import com.iats.model.response.UserResponse;
import com.iats.model.response.UserResponseList;
import com.iats.service.UserService;

@RestController
@RequestMapping(value = "/api/v1")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/getUsers")
    public ResponseEntity<?> getUsersList(@Valid @RequestBody UserRequest request) {

        UserResponse response = new UserResponse(Constant.STATUS_TRUE, Constant.SUCCESS);
        UserResponseList obj = userService.getUserList(request);
        response.setData(obj);
        return ResponseEntity.ok(response);
    }
}

Test the Application:

Using Postman, we’ll test the RESTful API POST method for getting users based on different filter parameters.

Inside the database, I’ve 3 records.

Below is the postman HTTP request in order to get the records from the database based on filter parameters. In the first example, I’m passing the request body with email, name, and gender. Data will display based on the parameters.

Test Example 1

In the second example, I’m passing only one parameter with a name and data will be display based on the name filter parameter. This name parameter can be first, middle, or last name, and the search will be implemented based on the name key.

Test Example 2

In the above example, two records are displaying based on the name key. It is searching key “er” in database columns (first, middle and last name) if it’s found then display the record in the response. All search has been implemented using Specification and Predicate.

Summary:

In this tutorial, you have learned how to use Spring Data Specifications and Predicates along with JPA Criteria API to dynamically generate database queries. Specifications provide us a robust and flexible approach to creating database queries to handle complex use cases. This tutorial covers basic operations that can be used to implement a powerful search feature.


2 Comments on "Specification & Predicate: Advance Search and Filtering in JPA"

Leave a Reply

Your email address will not be published. Required fields are marked *