领先一步
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 引入并由注释处理 API 生成的元模型类。此代码的主要问题是谓词不容易外部化和重用,因为您需要首先设置CriteriaBuilder
、CriteriaQuery
和Root
。此外,代码的可读性较差,因为很难在第一次查看时快速推断代码的意图。
为了能够定义可重用的Predicate
,我们引入了Specification
接口,该接口源自 Eric Evans 的领域驱动设计书籍中介绍的概念。它将规范定义为实体上的谓词,这正是我们的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));
}
};
}
}
诚然,这不是世界上最漂亮的代码,但它很好地满足了我们的初始需求:我们可以引用一组原子规范。下一个问题是:我们将如何执行这些规范?为此,您只需在存储库接口中扩展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
甚至无需进一步包装即可重用,这让我们摆脱了额外的(并且实现起来有点丑陋的)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平台。请参阅cloudfoundry-samples wiki以开始使用 Spring Data 和 CloudFoundry。