Simplification of Data Access Layer Implementation using JPA Specification

Mohammad Anik Islam
Monstar Lab Bangladesh Engineering
4 min readJul 24, 2018
Photo by David Kovalenko on Unsplash

While developing different projects, we often come across the problem of querying with various parameters for searching or filtering out entries in the database. While developing these kind of features we face two types of problems.

Firstly, we need to support a wide combination of substring search, greater than or less than condition, join queries, combine columns and so on.

Secondly, when the requirement changes, we have to modify huge chunks of code to facilitate new requirements.

JPA by default provides an interface to generate simple queries. But it cannot cope with complex queries. To mitigate this problem, JPA provides a very powerful tool, the JPA Specification for easing up the query generation dynamically.

Entity Models

For explanatory purpose, let us assume two entity classes Student and Course, which have a many to many relation with each other.

@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private String firstName;
private String lastName;
@OneToManyprivate List<Course> courses;

// getters and setters
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private String courseName;
@OneToManyprivate List<Student> students;

// getters and setters
}

Repository Interfaces

To define the repository, we will simply extend two interfaces namely, JpaRepository interface for basic jpa repository support and JpaSpecificationExecutor for executing JPA specifications.

public interface StudentRepository extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> { }public interface CourseRepository extends JpaRepository<Course, Integer>, JpaSpecificationExecutor<Course> { }

So, we will be able to use the inherited method like findOne(Specification<T> spec), findAll(Specification<T> spec), count(Specification<T> spec) of the JpaSpecificationExecutor interface.

Creating specification

Now let us create some specification to pass to the repository layers. To build specification using the constructor, we need to override the toPredicate method. For example, let us create a like specification for substring search. In this method, the parameter “key” is the property of the class and the parameter “value” contains the value to be checked against.

public Specification<T> getLikeSpecification(String key, String value) {return new Specification<T>() {@Overridepublic Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,    CriteriaBuilder builder) {  return builder.like(root.get(key).as(String.class), value);  }};}

For example, if we try to search students with first name having the pattern “abc” in them. Then we can implement it in the following way,

@Autowiredprivate StudentRepository studentRepository;Specification<Student> specification = getLikeSpecification(“firstName”, “abc”);List<Student> students = studentRepository.findAll(specification);

We can reuse the same method to find all courses with name having the pattern “xyz” in them.

@Autowiredprivate CourseRepository courseRepository;Specification<Course> specification = getLikeSpecification(“courseName”, “xyz”);List<Course> courses = courseRepository.findAll(specification);

The CriteriaBuilder class contains a wide array of methods like greater than, less than, in, between etc for creating all kinds of reusable specifications easily.

Specification for concatenation of columns

Sometimes we might need to concatenate two properties of the entity class and run a query on them. In the following example, the parameters, “key1” and “key2”, are the properties that will be concatenated and the parameter “value” contains the value to be checked against.

public Specification<T> getLikeSpecificationWithConcatenation(String key1, String key2, String value) {return new Specification<T>() {@Overridepublic Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {  return builder.like(builder.concat(root.get(key1),    root.get(key2)).as(String.class), value);
}};
}

Let us assume, we need to find all students with concatenation of first name and last name having the pattern “abc” in them. The implementation for that would be like this.

@Autowiredprivate StudentRepository studentRepository;Specification<Student> specification = getLikeSpecificationWithConcatenation(“firstName”, “lastName”, “abc”);List<Student> students = studentRepository.findAll(specification);

Specification for nested entity class properties

Sometimes we might need to run query on a base entity depending on a property of the nested entity. In the following method, the parameter “key” is the property of the nested entity, the parameter “joinEntity” denotes the name of the nested entity that must be contained in the base entity and the parameter “value” contains the value to be checked against.

public Specification<T> getLikeSpecificationWithJoin(String key, String joinEntity, String value) {return new Specification<T>() {@Overridepublic Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) { return builder.like(root.join(joinEntity).as(String.class), value); }};}

For example, we might need to find all students who have taken a specific course whose name contains the pattern “abc”. It can be done as following.

@Autowiredprivate StudentRepository studentRepository;Specification<Student> specification = getLikeSpecificationWithJoin(“courseName”, “courses”, “abc”);List<Student> students = studentRepository.findAll(specification);

Combining Specifications

So far we have learned different ways to generate specifications. But in some cases, we might need to combine these specifications using AND/OR condition.

For example, If we want to find students whose first name contains the pattern “abc” and last name contains the pattern “xyz”. First we can build 2 different specifications namely in the following way,

Specification<Student> specification1 = getLikeSpecification(“firstName”, “abc”);Specification<Student> specification2 = getLikeSpecification(“lastName”, “xyz”);

Then we can combine these two specifications using AND in the following way,

Specification<Student> specification = Specifications.where(specification1).and(specification2);List<Student> students = studentRepository.findAll(specification);

Conclusion

In this article, I have briefly tried to explain how to create reusable components to generate specifications and combine them to create complex queries programmatically.

On a regular basis, we, the developers, need to absorb new changes in requirements. The beauty of Specification is that we don’t need to write raw queries manually and whenever we need to absorb changes in requirements we can do it with minimal changes.

Specification builder can also be used to create a generic query language that suits all possible combinations fulfilling most of the search or filtering requirements. I will write more about that in my next article.

--

--