JPA使用Specification pattern 进行数据查
这篇文章介绍了在JPA中 如何使用specification pattern来查询数据库中所需要的数据。 主要是如何将JPA Criteria queries与specification pattern相结合来在关系型数据库中获取所需要的对象。
这里主要用一个Poll类(选举)作为一个实体类在生成specification。 这个实体类中有start date 与end date来表示选举的开始时间以及结束时间。在这期间用户可以发起vote, 也就是投票。 如果一轮选举还没有到达结束时间,但是被Anministrator主动关闭了,那么用lock data来代表关闭的时间。
@Entity
public class Poll {
@Id
@GeneratedValue
private long id;
private DateTime startDate;
private DateTime endDate;
private DateTime lockDate;
@OneToMany(cascade = CascadeType.ALL)
private List<Vote> votes = new ArrayList<>();
}
为了更好的可读性,在这里省略了各种setter以及getter方法
现在我们假设有两个约束需要实现来查询我们的数据库
- poll 这轮选举正在进行中 条件:没有主动被关闭同时 startdate<current time<enddate
- poll 是非常popular的 条件:没有主动被关闭 同时其中的投票超过了100
通常一般情况下 我们有两种方法, 要么写一个 poll.isCurrentlyRunning()方法或者使用service例如pollService.isCurrentlyRunning(poll). 但是这两个方法都是判断一个poll是否正在进行,如果我们的需求是在数据库中查询所有正在进行的poll,那么我们可能需要使用JPA提供的repository方法:pollRepository.findAllCurrentlyRunningPolls().
下面介绍了如何使用JPA提供的specification pattern来进行查询,并且同时结合以上两种约束来找到没有被关闭的popular的poll
首先需要一个创建一个specification 接口:
public interface Specification<T> {
boolean isSatisfiedBy(T t);
Predicate toPredicate(Root<T> root, CriteriaBuilder cb);
Class<T> getType();
}
然后写一个抽象类来继承这个接口,实现里面的方法:
abstract public class AbstractSpecification<T> implements Specification<T> {
@Override
public boolean isSatisfiedBy(T t) {
throw new NotImplementedException();
}
@Override
public Predicate toPredicate(Root<T> poll, CriteriaBuilder cb) {
throw new NotImplementedException();
}
@Override
public Class<T> getType() {
ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
return (Class<T>) type.getActualTypeArguments()[0];
}
}
这里先忽略掉getType()这个方法,之后会解释
这里最重要的方法就是 isSatisfiedBy(), 它主要是用来判断我们的对象是否符合所谓的specification,toPredicate 返回一个约束作为javax.persistence.criteria.Predicate的实例,这个约束主要是用来查询数据库的时候用的。
对于上述
- poll 这轮选举正在进行中 条件:没有主动被关闭同时 startdate<current time<enddate
- poll 是非常popular的 条件:没有主动被关闭 同时其中的投票超过了100
这两个查询条件,我们会生成两个新的specification的类(继承 AbstractSpecification<T> ),在其中具体的实现 isSatisfiedBy(T t) 和 toPredicate(Root<T> poll, CriteriaBuilder cb) 两个方法。
**IsCurrentlyRunning ** 判断这个poll是否当前正在进行,
public class IsCurrentlyRunning extends AbstractSpecification<Poll> {
@Override
public boolean isSatisfiedBy(Poll poll) {
return poll.getStartDate().isBeforeNow()
&& poll.getEndDate().isAfterNow()
&& poll.getLockDate() == null;
}
@Override
public Predicate toPredicate(Root<Poll> poll, CriteriaBuilder cb) {
DateTime now = new DateTime();
return cb.and(
cb.lessThan(poll.get(Poll_.startDate), now),
cb.greaterThan(poll.get(Poll_.endDate), now),
cb.isNull(poll.get(Poll_.lockDate))
);
}
}
在 isSatisfiedBy(Poll poll) 我们判断当前传进来的poll是否正在进行,在 toPredicate(Root<Poll> poll, CriteriaBuilder cb) 里面,主要我们的目的是利用一个JPA's CriteriaBuilder 构造一个 Predicate 实例,之后会使用这个实力在构建一个 CriteriaQuery 来查询数据库。cb.and() 与 &&相同。
在创建一个specification, IsPopular 判断这个poll是否是popular
public class IsPopular extends AbstractSpecification<Poll> {
@Override
public boolean isSatisfiedBy(Poll poll) {
return poll.getLockDate() == null && poll.getVotes().size() > 100;
}
@Override
public Predicate toPredicate(Root<Poll> poll, CriteriaBuilder cb) {
return cb.and(
cb.isNull(poll.get(Poll_.lockDate)),
cb.greaterThan(cb.size(poll.get(Poll_.votes)), 100)
);
}
}
现在如果测试给定一个poll的实例, 我们可以根据这个poll才生成这两个约束的specification同时判断是否满足条件:
boolean isPopular = new IsPopular().isSatisfiedBy(poll);
boolean isCurrentlyRunning = new IsCurrentlyRunning().isSatisfiedBy(poll);
我们需要拓展仓库类用来查询数据库。
public class PollRepository {
private EntityManager entityManager = ...
public <T> List<T> findAllBySpecification(Specification<T> specification) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// use specification.getType() to create a Root<T> instance
CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(specification.getType());
Root<T> root = criteriaQuery.from(specification.getType());
// get predicate from specification
Predicate predicate = specification.toPredicate(root, criteriaBuilder);
// set predicate and execute query
criteriaQuery.where(predicate);
return entityManager.createQuery(criteriaQuery).getResultList();
}
}
我们使用getType来创建 CriteriaQuery<T> 跟 Root<T> 实例。getType返回一个由子类定义的AbstractSpecification <T> 实例的通用类型。对于 IsPopular 和IsCurrentlyRunning,它返回Poll类。 没有getType(),我们将必须在我们创建的每个规范的toPredicate()中创建CriteriaQuery <T>和Root <T>实例。 所以它只是一个小的帮手,以减少规格内的重复代码。 如果你提出了更好的方法,请随意将其替换为你自己的实现。
到目前为止,specification只是我们一些约束的载体,它最主要的用途还是查询数据库或者检查一个对象是否满足特定的条件。
现在如果将这两个约束联合在一起成为一个条件,也就是说我们需要查询数据库来查询那些既满足是isrunning有满足popular的poll,这个时候 我们就需要 composite specifications。通过composite specifications 我们可以将不同的spefication结合在一起。
我们在创建一个新的specification类,
public class AndSpecification<T> extends AbstractSpecification<T> {
private Specification<T> first;
private Specification<T> second;
public AndSpecification(Specification<T> first, Specification<T> second) {
this.first = first;
this.second = second;
}
@Override
public boolean isSatisfiedBy(T t) {
return first.isSatisfiedBy(t) && second.isSatisfiedBy(t);
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaBuilder cb) {
return cb.and(
first.toPredicate(root, cb),
second.toPredicate(root, cb)
);
}
@Override
public Class<T> getType() {
return first.getType();
}
}
AndSpecification以两个specification做为构造器参数,在内部的 isSatisfiedBy()和 toPredicate()中,我们返回由逻辑和操作组合的两个规范的结果。
Specification<Poll> popularAndRunning = new AndSpecification<>(new IsPopular(), new IsCurrentlyRunning());
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);
为了提高可读性,我们可以在specification interface中添加一个add方法:
public interface Specification<T> {
Specification<T> and(Specification<T> other);
// other methods
}
在 AbstractSpecification<T> 中:
abstract public class AbstractSpecification<T> implements Specification<T> {
@Override
public Specification<T> add(Specification<T> other) {
return new AddSpecification<>(this, other);
}
// other methods
}
现在可以使用and()方法链接多个specification
Specification<Poll> popularAndRunning = new IsPopular().and(new IsCurrentlyRunning());
boolean isPopularAndRunning = popularAndRunning.isSatisfiedBy(poll);
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);
当需要时,可以使用其他复合材料规格(例如OrSpecification或NotSpecification)来进一步扩展specification。
总结:
当使用specification pattern时,我们将业务规则移到单独的specification类中。 这些specification类别可以通过使用 composite specifications 规格轻松组合。 一般来说,specification 提高了可重用性和可维护性。 另外specification 可以轻松进行单元测试。 有关specification pattern的更多详细信息,英语比较好的同学可以去读读Eric Evans和Martin Fowler的这篇文章。
本文章的源码在整理过程中,稍后放出。