欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

高级Spring Data JPA – 规范和Querydsl

程序员文章站 2022-04-25 20:01:59
...

在我上一篇博客文章中,我介绍了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第一。此外,代码的可读性很差,因为很难在第一眼就快速推断出代码的意图。

产品规格

为了能够定义可重用的Predicates,我们引入了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在存储库界面中进行扩展,从而“拉入”API以执行Specifications:

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

客户现在可以这样做:

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

基本的存储库实现将为您准备CriteriaQueryRootCriteriaBuilder为您应用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版本中。除此之外,CloudFoundry平台还支持Spring Data的Mongo和JPA模块。有关Spring Data和CloudFoundry的入门信息,请参阅cloudfoundry-samples wiki