在 Spring Data应用程序中使用Criteria条件查询

时间:2022-12-04 12:52:48

1. 简介

Spring Data JPA提供了许多处理实体的方法,包括查询方法和自定义JPQL查询。但有时,我们需要一种更具程序化的方法,例如标准APIQueryDSL

条件 API 提供了一种创建类型化查询的编程方式,这有助于我们避免语法错误。此外,当我们将其与元模型 API 一起使用时,它会进行编译时检查以确认我们是否使用了正确的字段名称和类型。

但是,它有其缺点;我们必须用样板代码编写冗长的逻辑。

在本教程中,我们将学习如何使用条件查询实现我们的自定义 DAO 逻辑。我们还将说明 Spring 如何帮助减少样板代码。

2. 示例应用程序

为了示例中的简单起见,我们将以多种方式实现相同的查询:按作者姓名和包含String 的标题查找书籍。

下面是Book实体:

@Entity
class Book {

    @Id
    Long id;
    String title;
    String author;

    // getters and setters

}

因为我们想让事情变得简单,所以我们在本教程中不会使用元模型 API。

3.@Repository

众所周知,在 Spring 组件模型中,我们应该将数据访问逻辑放在 @Repositorybean 中。当然,此逻辑可以使用任何实现,例如条件 API。

为此,我们只需要一个EntityManager实例,我们可以自动连线:

@Repository
class BookDao {

    EntityManager em;

    // constructor

    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Book> cq = cb.createQuery(Book.class);

        Root<Book> book = cq.from(Book.class);
        Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
        Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
        cq.where(authorNamePredicate, titlePredicate);

        TypedQuery<Book> query = em.createQuery(cq);
        return query.getResultList();
    }

}

上面的代码遵循标准条件 API 工作流:

  • 首先,我们得到一个CriteriaBuilder引用,我们可以使用它来创建查询的不同部分。
  • 使用CriteriaBuilder,我们创建了一个CriteriaQuery<Book>,它描述了我们要在查询中执行的操作。它还声明结果中行的类型。
  • 使用CriteriaQuery<Book>,我们声明查询的起点(Book实体),并将其存储在book变量*以后使用。
  • 接下来,使用CriteriaBuilder,我们针对Book实体创建谓词。请注意,这些谓词尚无任何效果。
  • 我们将这两个谓词应用于我们的CriteriaQuery。CriteriaQuery.where(谓词...)将其参数组合在一个逻辑.这就是我们将这些谓词与查询绑定时的重点。
  • 之后,我们从CriteriaQuery 创建一个TypedQuery<Book> 实例。
  • 最后,我们返回所有匹配的 Book实体。

请注意,由于我们用 @Repository 标记了 DAO 类,Spring 为该类启用了异常转换

4. 使用自定义方法扩展存储库

拥有自动自定义查询是一项强大的 Spring 数据功能。但是,有时我们需要更复杂的逻辑,而我们无法通过自动查询方法创建这些逻辑。

我们可以在单独的 DAO 类中实现这些查询(如上一节所述)。

或者,如果我们希望一个@Repository接口有一个带有自定义实现的方法,我们可以使用可组合的存储库

自定义界面如下所示:

interface BookRepositoryCustom {
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}

这是@Repository界面:

interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {}

我们还必须修改我们之前的 DAO 类来实现BookRepositoryCustom,并将其重命名为BookRepositoryImpl

@Repository
class BookRepositoryImpl implements BookRepositoryCustom {

    EntityManager em;

    // constructor

    @Override
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        // implementation
    }

}

当我们将 BookRepository 声明为依赖项时,Spring 会找到BookRepositoryImpl并在调用自定义方法时使用它。

假设我们要选择要在查询中使用的谓词。例如,当我们不想按作者和书名查找书籍时,我们只需要匹配作者即可。

有多种方法可以做到这一点,例如仅在传递的参数不为 null 时才应用谓词:

@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Book> cq = cb.createQuery(Book.class);

    Root<Book> book = cq.from(Book.class);
    List<Predicate> predicates = new ArrayList<>();
    
    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));

    return em.createQuery(cq).getResultList();
}

但是,这种方法使代码难以维护,特别是如果我们有许多谓词并希望使它们成为可选的。

将这些谓词外部化将是一个实用的解决方案。有了 JPA 规范,我们可以做到这一点,甚至更多。

5. 使用 JPA 规范

Spring Data 引入了org.springframework.data.jpa.domain.Specification接口来封装单个谓词:

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

我们可以提供创建规范实例的方法:

static Specification<Book> hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}

static Specification<Book> titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}

要使用它们,我们需要我们的存储库来扩展org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>

interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {}

此接口声明了使用规范的便捷方法。例如,现在我们可以使用以下单行代码找到指定作者的所有Book实例:

bookRepository.findAll(hasAuthor(author));

不幸的是,我们没有得到任何可以传递多个规范参数的方法。相反,我们在org.springframework.data.jpa.domain.Specification接口中获取实用程序方法。

例如,我们可以将两个规范实例与一个逻辑

bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));

在上面的例子中,where()规范类的静态方法。

这样我们就可以使查询模块化。此外,我们不必编写标准API样板,因为Spring为我们提供了它。

请注意,这并不意味着我们不必再编写标准样板;这种方法只能处理我们看到的工作流,即选择满足所提供条件的实体。

查询可以有许多它不支持的结构,包括分组、返回与我们选择的不同类或子查询。

6. 结论

在本文中,我们讨论了在 Spring 应用程序中使用条件查询的三种方法:

  • 创建 DAO 类是最直接、最灵活的方法。
  • 扩展@Repository接口,与自动查询无缝集成
  • 规范实例中使用谓词,使简单案例更简洁、不那么冗长

像往常一样,这些示例可以在GitHub上找到。