JavaEE 7教程

使用JPA实现Specification规范规格模式

  由DDD之父 Eric Evans 和OO之父 Martin Fowler定义的规范(Specification也称规格模式)模式article 越来越受到广泛应用,本文介绍如何使用JavaEE 持久层规范JPA实现规格模式,其实现思想也适合其他持久层框架。案例源码见GitHub

  在这个文章中,我们将使用以下POLL类作为创建Specification为例实体。它代表了民意调查,有一个开始和结束日期。在这两个日期之间的时间内用户可以在不同的选择之间投票表决Poll,投票Poll可以在管理员的结束日期尚未到达前锁定。在这种情况下,一个锁日期将被设置。

@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<>();

}

现在我们有两个约束:

  • 一个投票poll如果满足startDate < now < endDate,那表示它在进行中。
  • 一个投票poll如果超过100个投票但是没有锁定,表示它很流行。

  我们可以通过在POLL投票添加适当的方法如:poll.isCurrentlyRunning(),另外,我们可以使用一个服务方法pollService.isCurrentlyRunning(进行轮询)。然而,我们也希望能够查询数据库获得所有当前正在运行的民意调查。因此,我们可以添加一个DAO或储存repository库的方法类似pollRepository.findAllCurrentlyRunningPolls()

  如果我们想结合上述两个约束查询,例如我们想在数据库中查询当前正在运行的所有流行的调查名单?这时Specification模式派上用场。当使用Specification模式时,我们将业务规则转换为额外的类称为Specification。

  创建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];
  }
}

specification的核心是isSatisfiedBy() 方法,用于检查一个对象是否满足规范规格要求。. toPredicate()是一个附加的方法,我们在案例中使用它返回一个约束 javax.persistence.criteria.Predicate 实例,它能被用于查询一个数据库。

  检查一个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))
    );
  }
}

检查一个投票是否流行的代码如下:

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)), 5)
    );
  }
}

使用代码:

boolean isPopular = new IsPopular().isSatisfiedBy(poll);
boolean isCurrentlyRunning = new IsCurrentlyRunning().isSatisfiedBy(poll);

为了查询数据库,我们需要继承扩展DAO/Repository来支持规范:

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();
  }
}

我们使用 AbstractSpecification<T>的getType() 方法 创建CriteriaQuery<T> 和 Root<T> 实例. getType()返回的是一个 AbstractSpecification<T>泛型,可以由子类定义. 对于流行IsPopular 且正在进行IsCurrentlyRunning它返回一个Poll class. 如果没有 getType()我们必须在每个规范的 toPredicate() 中实现创建 CriteriaQuery<T> 和 Root<T>实例。它只是减少烦人代码的帮助者。

使用代码:

List<Poll> popularPolls = pollRepository.findAllBySpecification(new IsPopular());
List<Poll> currentlyRunningPolls = pollRepository.findAllBySpecification(new IsCurrentlyRunning());

下面问题是如何结合这两个规范,我们如何查询数据库所有流行且还在进行的投票呢?

使用组合模式实现组合规范模式。名称为AndSpecification:

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();
  }
}

使用代码:

Specification<Poll> popularAndRunning = new AndSpecification<>(new IsPopular(), new IsCurrentlyRunning());
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);

为了提供可读性,我们增加and方法到规范接口

public interface Specification<T> {

  Specification<T> and(Specification<T> other);

  // other methods
}

abstract public class AbstractSpecification<T> implements Specification<T> {

  @Override
  public Specification<T> and(Specification<T> other) {
    return new AndSpecification<>(this, other);
  }

  // other methods
}

使用代码:

Specification<Poll> popularAndRunning = new IsPopular().and(new IsCurrentlyRunning());
boolean isPopularAndRunning = popularAndRunning.isSatisfiedBy(poll);
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);

 

 

规格模式

Java学习心得