抢先一步
VMware 提供培训和认证,助你快速提升。
了解更多在我上一篇博文中,我介绍了 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 生成的元模型类。这段代码的主要问题是谓词不容易外部化和重用,因为你需要先设置 CriteriaBuilder
、CriteriaQuery
和 Root
。此外,代码的可读性很差,因为很难快速推断代码的意图。
为了能够定义可重用的 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());
基本的仓库实现将为你准备 CriteriaQuery
、Root
和 CriteriaBuilder
,应用由给定 Specification
创建的 Predicate
并执行查询。但是,我们不是可以通过创建简单的查询方法来实现吗?没错,但请记住我们的第二个初始需求。我们希望能够自由组合原子 Specification
,以便动态创建新的 Specification。为此,我们有一个辅助类 Specifications
,它提供了 and(…)
和 or(…)
方法来连接原子 Specification
。还有一个 where(…)
方法提供一些语法糖,使表达式更具可读性。我最初提出的用例示例如下:
customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));
这读起来很流畅,提高了可读性,同时也比单独使用 JPA Criteria API 提供了额外的灵活性。这里唯一的缺点是,实现 Specification
需要相当多的编码工作。
为了解决这个问题,一个名为 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));
这不仅几乎是开箱即用的流畅英语,而且 BooleanExpression
s 甚至无需进一步包装即可重用,这使我们摆脱了额外的(而且实现起来有点难看)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。