Advanced Spring Data JPA - Specifications and 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的Domain Driven Design一书中介绍的概念。它将规范定义为实体上的谓词,这正是我们的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());

基本的仓库实现将为您准备CriteriaQueryRootCriteriaBuilder,应用给定Specification创建的Predicate并执行查询。但是我们不能简单地创建简单的查询方法来实现这一点吗?是的,但请记住我们的第二个初始需求。我们希望能够自由地组合原子Specifications来即时创建新的Specifications。为此,我们有一个辅助类Specifications,它提供了and(...)or(...)方法来连接原子Specifications。还有一个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平台。请参阅cloudfoundry-samples wiki以开始使用Spring Data和CloudFoundry。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

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

了解更多

即将举行的活动

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

查看所有