Spring Data JPA提供了许多处理实体的方法,包括查询方法和自定义JPQL查询。但有时,我们需要一种更具程序化的方法,例如标准API或QueryDSL。
条件 API 提供了一种创建类型化查询的编程方式,这有助于我们避免语法错误。此外,当我们将其与元模型 API 一起使用时,它会进行编译时检查以确认我们是否使用了正确的字段名称和类型。
但是,它有其缺点;我们必须用样板代码编写冗长的逻辑。
在本教程中,我们将学习如何使用条件查询实现我们的自定义 DAO 逻辑。我们还将说明 Spring 如何帮助减少样板代码。
为了示例中的简单起见,我们将以多种方式实现相同的查询:按作者姓名和包含String 的标题查找书籍。
下面是Book实体:
- @Entity
- class Book {
- @Id
- Long id;
- String title;
- String author;
- // getters and setters
- }
因为我们想让事情变得简单,所以我们在本教程中不会使用元模型 API。
众所周知,在 Spring 组件模型中,我们应该将数据访问逻辑放在 @Repositorybean 中。当然,此逻辑可以使用任何实现,例如条件 API。
为此,我们只需要一个EntityManager实例,我们可以自动连线:
- @Repository
- class BookDao {
- EntityManager em;
- // constructor
- List
findBooksByAuthorNameAndTitle(String authorName, String title) { - CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery
cq = cb.createQuery(Book.class); - Root
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
query = em.createQuery(cq); - return query.getResultList();
- }
- }
上面的代码遵循标准条件 API 工作流:
请注意,由于我们用 @Repository 标记了 DAO 类,Spring 为该类启用了异常转换。
拥有自动自定义查询是一项强大的 Spring 数据功能。但是,有时我们需要更复杂的逻辑,而我们无法通过自动查询方法创建这些逻辑。
我们可以在单独的 DAO 类中实现这些查询(如上一节所述)。
或者,如果我们希望一个@Repository接口有一个带有自定义实现的方法,我们可以使用可组合的存储库。
自定义界面如下所示:
- interface BookRepositoryCustom {
- List
findBooksByAuthorNameAndTitle(String authorName, String title); - }
这是@Repository界面:
interface BookRepository extends JpaRepository, BookRepositoryCustom {}
我们还必须修改我们之前的 DAO 类来实现BookRepositoryCustom,并将其重命名为BookRepositoryImpl:
- @Repository
- class BookRepositoryImpl implements BookRepositoryCustom {
- EntityManager em;
- // constructor
- @Override
- List
findBooksByAuthorNameAndTitle(String authorName, String title) { - // implementation
- }
- }
当我们将 BookRepository 声明为依赖项时,Spring 会找到BookRepositoryImpl并在调用自定义方法时使用它。
假设我们要选择要在查询中使用的谓词。例如,当我们不想按作者和书名查找书籍时,我们只需要匹配作者即可。
有多种方法可以做到这一点,例如仅在传递的参数不为 null 时才应用谓词:
- @Override
- List
findBooksByAuthorNameAndTitle(String authorName, String title) { - CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery
cq = cb.createQuery(Book.class); - Root
book = cq.from(Book.class); - List
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 规范,我们可以做到这一点,甚至更多。
Spring Data 引入了org.springframework.data.jpa.domain.Specification接口来封装单个谓词:
- interface Specification
{ - Predicate toPredicate(Root
root, CriteriaQuery query, CriteriaBuilder cb); - }
我们可以提供创建规范实例的方法:
- static Specification
hasAuthor(String author) { - return (book, cq, cb) -> cb.equal(book.get("author"), author);
- }
- static Specification
titleContains(String title) { - return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
- }
要使用它们,我们需要我们的存储库来扩展org.springframework.data.jpa.repository.JpaSpecificationExecutor
interface BookRepository extends JpaRepository, JpaSpecificationExecutor {}
此接口声明了使用规范的便捷方法。例如,现在我们可以使用以下单行代码找到指定作者的所有Book实例:
bookRepository.findAll(hasAuthor(author));
不幸的是,我们没有得到任何可以传递多个规范参数的方法。相反,我们在org.springframework.data.jpa.domain.Specification接口中获取实用程序方法。
例如,我们可以将两个规范实例与一个逻辑和:
bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));
在上面的例子中,where()是规范类的静态方法。
这样我们就可以使查询模块化。此外,我们不必编写标准API样板,因为Spring为我们提供了它。
请注意,这并不意味着我们不必再编写标准样板;这种方法只能处理我们看到的工作流,即选择满足所提供条件的实体。
查询可以有许多它不支持的结构,包括分组、返回与我们选择的不同类或子查询。
在本文中,我们讨论了在 Spring 应用程序中使用条件查询的三种方法:
像往常一样,这些示例可以在GitHub上找到。