這篇文章介紹了在JPA中 如何使用specification pattern來(lái)查詢(xún)數(shù)據(jù)庫(kù)中所需要的數(shù)據(jù)。 主要是如何將JPA Criteria queries與specification pattern相結(jié)合來(lái)在關(guān)系型數(shù)據(jù)庫(kù)中獲取所需要的對(duì)象。
這里主要用一個(gè)Poll類(lèi)(選舉)作為一個(gè)實(shí)體類(lèi)在生成specification。 這個(gè)實(shí)體類(lèi)中有start date 與end date來(lái)表示選舉的開(kāi)始時(shí)間以及結(jié)束時(shí)間。在這期間用戶(hù)可以發(fā)起vote, 也就是投票。 如果一輪選舉還沒(méi)有到達(dá)結(jié)束時(shí)間,但是被Anministrator主動(dòng)關(guān)閉了,那么用lock data來(lái)代表關(guān)閉的時(shí)間。
@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方法
現(xiàn)在我們假設(shè)有兩個(gè)約束需要實(shí)現(xiàn)來(lái)查詢(xún)我們的數(shù)據(jù)庫(kù)
- poll 這輪選舉正在進(jìn)行中 條件:沒(méi)有主動(dòng)被關(guān)閉同時(shí) startdate<current time<enddate
- poll 是非常popular的 條件:沒(méi)有主動(dòng)被關(guān)閉 同時(shí)其中的投票超過(guò)了100
通常一般情況下 我們有兩種方法, 要么寫(xiě)一個(gè) poll.isCurrentlyRunning()方法或者使用service例如pollService.isCurrentlyRunning(poll). 但是這兩個(gè)方法都是判斷一個(gè)poll是否正在進(jìn)行,如果我們的需求是在數(shù)據(jù)庫(kù)中查詢(xún)所有正在進(jìn)行的poll,那么我們可能需要使用JPA提供的repository方法:pollRepository.findAllCurrentlyRunningPolls().
下面介紹了如何使用JPA提供的specification pattern來(lái)進(jìn)行查詢(xún),并且同時(shí)結(jié)合以上兩種約束來(lái)找到?jīng)]有被關(guān)閉的popular的poll
首先需要一個(gè)創(chuàng)建一個(gè)specification 接口:
public interface Specification<T> {
boolean isSatisfiedBy(T t);
Predicate toPredicate(Root<T> root, CriteriaBuilder cb);
Class<T> getType();
}
然后寫(xiě)一個(gè)抽象類(lèi)來(lái)繼承這個(gè)接口,實(shí)現(xiàn)里面的方法:
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()這個(gè)方法,之后會(huì)解釋
這里最重要的方法就是 isSatisfiedBy(), 它主要是用來(lái)判斷我們的對(duì)象是否符合所謂的specification,toPredicate 返回一個(gè)約束作為javax.persistence.criteria.Predicate的實(shí)例,這個(gè)約束主要是用來(lái)查詢(xún)數(shù)據(jù)庫(kù)的時(shí)候用的。
對(duì)于上述
- poll 這輪選舉正在進(jìn)行中 條件:沒(méi)有主動(dòng)被關(guān)閉同時(shí) startdate<current time<enddate
- poll 是非常popular的 條件:沒(méi)有主動(dòng)被關(guān)閉 同時(shí)其中的投票超過(guò)了100
這兩個(gè)查詢(xún)條件,我們會(huì)生成兩個(gè)新的specification的類(lèi)(繼承 AbstractSpecification<T> ),在其中具體的實(shí)現(xiàn) isSatisfiedBy(T t) 和 toPredicate(Root<T> poll, CriteriaBuilder cb) 兩個(gè)方法。
**IsCurrentlyRunning ** 判斷這個(gè)poll是否當(dāng)前正在進(jìn)行,
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) 我們判斷當(dāng)前傳進(jìn)來(lái)的poll是否正在進(jìn)行,在 toPredicate(Root<Poll> poll, CriteriaBuilder cb) 里面,主要我們的目的是利用一個(gè)JPA's CriteriaBuilder 構(gòu)造一個(gè) Predicate 實(shí)例,之后會(huì)使用這個(gè)實(shí)力在構(gòu)建一個(gè) CriteriaQuery 來(lái)查詢(xún)數(shù)據(jù)庫(kù)。cb.and() 與 &&相同。
在創(chuàng)建一個(gè)specification, IsPopular 判斷這個(gè)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)
);
}
}
現(xiàn)在如果測(cè)試給定一個(gè)poll的實(shí)例, 我們可以根據(jù)這個(gè)poll才生成這兩個(gè)約束的specification同時(shí)判斷是否滿(mǎn)足條件:
boolean isPopular = new IsPopular().isSatisfiedBy(poll);
boolean isCurrentlyRunning = new IsCurrentlyRunning().isSatisfiedBy(poll);
我們需要拓展倉(cāng)庫(kù)類(lèi)用來(lái)查詢(xún)數(shù)據(jù)庫(kù)。
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來(lái)創(chuàng)建 CriteriaQuery<T> 跟 Root<T> 實(shí)例。getType返回一個(gè)由子類(lèi)定義的AbstractSpecification <T> 實(shí)例的通用類(lèi)型。對(duì)于 IsPopular 和IsCurrentlyRunning,它返回Poll類(lèi)。 沒(méi)有getType(),我們將必須在我們創(chuàng)建的每個(gè)規(guī)范的toPredicate()中創(chuàng)建CriteriaQuery <T>和Root <T>實(shí)例。 所以它只是一個(gè)小的幫手,以減少規(guī)格內(nèi)的重復(fù)代碼。 如果你提出了更好的方法,請(qǐng)隨意將其替換為你自己的實(shí)現(xiàn)。
到目前為止,specification只是我們一些約束的載體,它最主要的用途還是查詢(xún)數(shù)據(jù)庫(kù)或者檢查一個(gè)對(duì)象是否滿(mǎn)足特定的條件。
現(xiàn)在如果將這兩個(gè)約束聯(lián)合在一起成為一個(gè)條件,也就是說(shuō)我們需要查詢(xún)數(shù)據(jù)庫(kù)來(lái)查詢(xún)那些既滿(mǎn)足是isrunning有滿(mǎn)足popular的poll,這個(gè)時(shí)候 我們就需要 composite specifications。通過(guò)composite specifications 我們可以將不同的spefication結(jié)合在一起。
我們?cè)趧?chuàng)建一個(gè)新的specification類(lèi),
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以?xún)蓚€(gè)specification做為構(gòu)造器參數(shù),在內(nèi)部的 isSatisfiedBy()和 toPredicate()中,我們返回由邏輯和操作組合的兩個(gè)規(guī)范的結(jié)果。
Specification<Poll> popularAndRunning = new AndSpecification<>(new IsPopular(), new IsCurrentlyRunning());
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);
為了提高可讀性,我們可以在specification interface中添加一個(gè)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
}
現(xiàn)在可以使用and()方法鏈接多個(gè)specification
Specification<Poll> popularAndRunning = new IsPopular().and(new IsCurrentlyRunning());
boolean isPopularAndRunning = popularAndRunning.isSatisfiedBy(poll);
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);
當(dāng)需要時(shí),可以使用其他復(fù)合材料規(guī)格(例如OrSpecification或NotSpecification)來(lái)進(jìn)一步擴(kuò)展specification。
總結(jié):
當(dāng)使用specification pattern時(shí),我們將業(yè)務(wù)規(guī)則移到單獨(dú)的specification類(lèi)中。 這些specification類(lèi)別可以通過(guò)使用 composite specifications 規(guī)格輕松組合。 一般來(lái)說(shuō),specification 提高了可重用性和可維護(hù)性。 另外specification 可以輕松進(jìn)行單元測(cè)試。 有關(guān)specification pattern的更多詳細(xì)信息,英語(yǔ)比較好的同學(xué)可以去讀讀Eric Evans和Martin Fowler的這篇文章。
本文章的源碼在整理過(guò)程中,稍后放出。