Spring Data JPA 高级特性 - Specifications 与 Querydsl

工程 | Oliver Drotbohm | 2011 年 4 月 26 日 | ...

在我上一篇博文中,我介绍了 Spring Data JPA 的基本功能集。在本篇博文中,我将深入探讨更多功能,以及它们如何帮助你进一步简化数据访问层实现。Spring Data 仓库抽象包含一个基于接口的编程模型、一些工厂类以及一个 Spring 命名空间,以便轻松配置基础设施。一个典型的仓库接口看起来像这样:

public interface CustomerRepository extends JpaRepository<Customer, Long> {

  Customer findByEmailAddress(String emailAddress);

  List<Customer> findByLastname(String lastname, Sort sort);

  Page<Customer> findByFirstname(String firstname, Pageable pageable);
}

第一个方法简单地查找具有给定电子邮件地址的单个客户,第二个方法返回所有具有给定姓氏的客户并对结果应用给定的 Sort,而第三个方法返回客户的 Page。有关详细信息,请参阅前一篇博文

虽然这种方法非常方便(你甚至无需编写一行实现代码即可执行查询),但它有两个缺点:首先,由于查询定义了一组固定的条件(这是第二点),对于大型应用程序而言,查询方法的数量可能会增加。为了避免这两个缺点,如果你可以提出一组原子谓词,并能动态地组合它们来构建查询,那岂不是很酷?

如果你是长期使用 JPA 的用户,你可能会问:Criteria API 不就是为了这个吗?没错,那么让我们看看使用 JPA Criteria API 实现一个示例业务需求是什么样子的。用例是:在客户生日时,我们想给所有长期客户发送一张优惠券。我们如何检索符合条件的客户?

谓词主要有两个部分:生日以及我们称之为长期客户的条件。让我们假设后者意味着客户账户至少在两年前创建。以下是使用 JPA 2.0 Criteria API 实现的代码:

LocalDate today = new LocalDate();

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Customer> query = builder.createQuery(Customer.class);
Root<Customer> root = query.from(Customer.class);

Predicate hasBirthday = builder.equal(root.get(Customer_.birthday), today);
Predicate isLongTermCustomer = builder.lessThan(root.get(Customer_.createdAt), today.minusYears(2); 
query.where(builder.and(hasBirthday, isLongTermCustomer));
em.createQuery(query.select(root)).getResultList();

这里有什么呢?为了方便,我们创建一个新的 LocalDate,然后接着是三行样板代码来设置必要的 JPA 基础设施实例。然后是两行构建谓词的代码,一行用来连接两者,最后一行用来执行实际的查询。我们使用了 JPA 2.0 引入并由 Annotation Processing API 生成的元模型类。这段代码的主要问题是谓词不容易外部化和重用,因为你需要先设置 CriteriaBuilderCriteriaQueryRoot。此外,代码的可读性很差,因为很难快速推断代码的意图。

Specifications

为了能够定义可重用的 Predicate,我们引入了 Specification 接口,该接口源自 Eric Evans 领域驱动设计一书中引入的概念。它将 Specification 定义为对实体的一个谓词,这正是我们的 Specification 接口所代表的。它实际上只包含一个方法:

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

因此我们现在可以很容易地使用这样的辅助类:

public CustomerSpecifications {

  public static Specification<Customer> customerHasBirthday() {
    return new Specification<Customer> {
      public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
        return cb.equal(root.get(Customer_.birthday), today);
      }
    };
  }

  public static Specification<Customer> isLongTermCustomer() {
    return new Specification<Customer> {
      public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
        return cb.lessThan(root.get(Customer_.createdAt), new LocalDate.minusYears(2));
      }
    };
  }
}

诚然,这不是世界上最漂亮的代码,但它很好地满足了我们最初的需求:我们可以引用一组原子 Specification。下一个问题是:我们将如何执行这些 Specification?为此,你只需在你的仓库接口中继承 JpaSpecificationExecutor,从而“引入”一个执行 Specification 的 API:

public interface CustomerRepository extends JpaRepository<Customer>, JpaSpecificationExecutor {
  // Your query methods here
}

客户端现在可以这样做:

customerRepository.findAll(hasBirthday());
customerRepository.findAll(isLongTermCustomer());

基本的仓库实现将为你准备 CriteriaQueryRootCriteriaBuilder,应用由给定 Specification 创建的 Predicate 并执行查询。但是,我们不是可以通过创建简单的查询方法来实现吗?没错,但请记住我们的第二个初始需求。我们希望能够自由组合原子 Specification,以便动态创建新的 Specification。为此,我们有一个辅助类 Specifications,它提供了 and(…)or(…) 方法来连接原子 Specification。还有一个 where(…) 方法提供一些语法糖,使表达式更具可读性。我最初提出的用例示例如下:

customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));

这读起来很流畅,提高了可读性,同时也比单独使用 JPA Criteria API 提供了额外的灵活性。这里唯一的缺点是,实现 Specification 需要相当多的编码工作。

Querydsl

为了解决这个问题,一个名为 Querydsl 的开源项目提出了一种非常相似但又不同的方法。就像 JPA Criteria API 一样,它使用 Java 6 注解处理器来生成元模型对象,但它生成了一个更易于使用(更亲近)的 API。该项目的另一个很酷之处在于,它不仅支持 JPA,还允许查询 Hibernate、JDO、Lucene、JDBC 甚至普通集合。

因此,要使其正常工作,你需要将 Querydsl 添加到你的 pom.xml 中,并相应地配置 APT 插件。

<plugin>
  <groupId>com.mysema.maven</groupId>
  <artifactId>maven-apt-plugin</artifactId>
  <version>1.0</version>
  <executions>
    <execution>
      <phase>generate-sources</phase>
      <goals>
        <goal>process</goal>
      </goals>
      <configuration>
        <outputDirectory>target/generated-sources</outputDirectory>
        <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
      </configuration>
    </execution>
  </executions>
</plugin>

这将导致你的构建创建特殊的查询类——在我们的例子中是同一包内的 QCustomer

QCustomer customer = QCustomer.customer;
LocalDate today = new LocalDate();
BooleanExpression customerHasBirthday = customer.birthday.eq(today);
BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));

这不仅几乎是开箱即用的流畅英语,而且 BooleanExpressions 甚至无需进一步包装即可重用,这使我们摆脱了额外的(而且实现起来有点难看)Specification 包装器。另外一个优点是,你可以在赋值语句右侧的每个点处获得 IDE 代码补全,所以 customer. + CTRL + SPACE 将列出所有属性。customer.birthday. + CTRL + SPACE 将列出所有可用的关键字,等等。要执行 Querydsl 谓词,你只需让你的仓库继承 QueryDslPredicateExecutor

public interface CustomerRepository extends JpaRepository<Customer>, QueryDslPredicateExecutor {
  // Your query methods here
}

客户端就可以简单地这样做:

BooleanExpression customerHasBirthday = customer.birthday.eq(today);
BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));
customerRepository.findAll(customerHasBirthday.and(isLongTermCustomer));

总结

Spring Data JPA 仓库抽象允许通过包装到 Specification 对象中的 JPA Criteria API 谓词或通过 Querydsl 谓词来执行谓词。要启用此功能,你只需让你的仓库继承 JpaSpecificationExecutor 或 QueryDslPredicateExecutor(如果你愿意,甚至可以同时使用两者)。请注意,如果你选择 Querydsl 方法,则需要在类路径中包含 Querydsl JAR 包。

还有一件事

Querydsl 方法的另一个很酷之处在于,它不仅适用于我们的 JPA 仓库,也适用于我们的 MongoDB 支持。此功能已包含在刚刚发布的 Spring Data MongoDB M2 版本中。除此之外,Spring Data 的 Mongo 和 JPA 模块均支持 CloudFoundry 平台。有关 Spring Data 和 CloudFoundry 的入门信息,请参阅 cloudfoundry-samples wiki

订阅 Spring Newsletter

订阅 Spring newsletter,保持联系

订阅

抢先一步

VMware 提供培训和认证,助你快速提升。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅,提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区所有即将举行的活动。

查看全部