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(), 它主要是用来判断我们的对象是否符合所谓的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的这篇文章

本文章的源码在整理过程中,稍后放出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容