转]Simplification of Data Access Layer Implementation using JPA Specification

原文地址 点击跳转

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;
@OneToMany
private List<Course> courses;
 
 // getters and setters
}
@Entity
public class Course {
@Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 private Long id;
 
 private String courseName;
@OneToMany
private 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>() {
@Override
public 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,

@Autowired
private 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.

@Autowired
private 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>() {
@Override
public 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.

@Autowired
private 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>() {
@Override
public 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.

@Autowired
private 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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值