JPA使用Specification pattern 進行資料查詢

weixin_34075551發表於2017-08-26

這篇文章介紹了在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(), 它主要是用來判斷我們的物件是否符合所謂的specificationtoPredicate 返回一個約束作為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> 例項的通用型別。對於 IsPopularIsCurrentlyRunning,它返回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的這篇文章

本文章的原始碼在整理過程中,稍後放出。

相關文章