分类
Spring Data

Spring Data JPA 条件查询

Use Criteria Queries in a Spring Data Application

1. 简介

Spring Data JPA中提供了多种数据查询的方式,比如说定义查询方法或使用自定义的JPQL。但有些时候,上述查询方式并不能够很好的满足我们对复杂综合查询的需要,这时候就需要Criteria APIQueryDSL登场了。

Criteria API提供了一种使用代码创建动态查询语句的功能,相较于编写传统的SQL语句,该方法能够有效的避免一些语法错误。当结合Metamodel API使用时,甚至可以在编译的阶段发现一些字段或类型的错误。

当然,凡是都有两面性,它的缺点是:即使实现一个看似不起眼的查询方法,也需要编写长篇的样板(只所以称为样板是由于每次写的都差不多)代码,这使得阅读代码的时候不是很爽;再者由于需要接受一些新的知识,所以对于初次接触Criteria API的小伙伴们并不友好。

本文将讨论如何使用criteria queries(条件查询)来实现自定义的数据访问逻辑,以及Spring是如果帮助我们减少样板代码的。

2. 需求假设

假设如下图书(Book)实体,我们需要实现的功能是:查询某个作者(author)并且图书名称包含特定的关键字的所有图书。

/**
 * 图书
 */
@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 名称
     */
    String title;

    /**
     * 作者
     */
    String author;
    // setter and getter
}

本着一事一议、尽量减少干扰项的原则,本文引入Metamodel API。

3. 数据仓库类

按Spring规范,我们使用@Repository 来标识自定义数据仓库。要实现Criteria API还需要在数据仓库中注入EntityManager实例:

/**
 * 图书数据访问对象
 */
@Repository
public class BookDao {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    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();
    }
}

上述代码遵循了标准的Criteria API 工作流:

  • 首先获取了一个CriteriaBuilderCriteriaBuilder是整个查询功能的基础。
  • 使用CriteriaBuilder创建了一个CriteriaQuery<Book>,泛型中的Book指明了我们想获取的数据是Book实体。
  • 使用CriteriaQuery<Book>的from方法来获取了一个Root<Book> book,指定了此次查询的起点为Book实体(对应book) 数据表。
  • 接下来使用CriteriaBuilder创建了两个基于Book实体的查询条件(仅仅是创建了一个查询条件,只有将其放入正式的查询中,该查询条件才会生效)。查询条件一用于查找book表中的author字段与传入authorname完全相同的记录;条询条件二用于查找book表的title字段左右模糊匹配title字段。
  • cp.where将两个查询条件组合到一起,表示:查询满足查询条件一并且同时满足查询条件二的数据。
  • 然后使用CriteriaQuery创建了一个TypedQuery<Book>实例,该实例具备了数据查询的能力。
  • 最后调用了TypedQuery的getResultList()方法,此时将综合查询条件进行数据查询,并且将获取的数据依次填充到CriteriaQuery<Book>上规定的<Book>实体上。

值得提出的一点时,由于我们在当前数据访问对象BookDao使用@Repository注解,所以spring将默认托管该类所产生的异常。

4. 使用自定义的查询的方法扩展数据仓库层

虽然我们可以使用Spring Data中根据方法名称自动创建动态查询的功能来完成一些个性化的查询工作。但当有一些些逻辑处理参杂在查询过程中时(比如对null值进行过滤),这种自动创建查询方法的功能便力不从心了。

当然了,我们完全可以像上一节展示的一样来创建自定义的数据仓库来实现一些复要的查询功能。

但我想说的是,参考Spring Data组合数据仓库一文将一些自定义的方法添加到JPA的@Repository接口机制,使用继承接口的方法来完成数据创建的初始化,会有一种更好开发体验。

比如我们创建如下BookRepositoryCustom接口:

/**
 * 自定义的Book数据仓库
 */
public interface BookRepositoryCustom {
    /**
     * 查询图书
     * @param authorName 作者名(精确)
     * @param title 图书名称(模糊)
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}

然后在创建的数据仓库接口中继承BookRepositoryCustom:

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

值得注意的是,如果想使用BookDao中的findBooksByAuthorNameAndTitle方法做为BookRepositoryCustom中方法的实现,则必须将BookDao重命名为BookRepositoryImpl,同时实现BookRepositoryCustom接口。

在此,我们保持原BookDao名称不变。并新建BookRepositoryImpl:

/**
 * 图书数据访问对象
 */
@Repository
public class BookRepositoryImpl implements BookRepositoryCustom {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    @Override
    public List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    }
}

然后使用另外一种方法来实现查询的功能,该功能将对排除掉值为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 Specifications来掀开这又臭又长的裹脚布。

5. JPA Specifications

Spring Data 提供了org.springframework.data.jpa.domain.Specification来简化查询操作时的样板代码:

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

该接口提供了toPredicate方法,在方法中注入了样板代码中需要的root、CriteriaQuery以及CriteriaBuilder。我们可以利用该接口来快速的创建查询条件:

/**
 * 查询作者
 * @param author 作者
 * @return
 */
static Specification<Book> hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}
 
/**
 * 查询图书名
 * @param title 名称
 * @return
 */
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> {}

此时,当前数据仓库层便能够处理Spring Data的Specification查询条件了。此时,如若获取某个作者的所有图书,则可以简短的这样写:

bookRepository.findAll(hasAuthor(author));

美中不足的是,JpaSpecificationExecutor提供的findAll仅仅支持传入一个Specification查询条件。幸运的是我们可以使用org.springframework.data.jpa.domain.Specification接口提供的方法来完成查询条件的组合。

比如将作者与图书名两个查询条件组合在一起进行综合查询:

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

上述代码中,我们使用了Specification接口上的where方法来完成了两个查询条件的组合。

这样以来,得益于伟大的Spring,我们在实现了查询模块化(可以轻易的按某一个或多个条件进行组合查询)的同时,还让代码看起来没有那么臃肿。

当然了,这并不意味着我们构造综合查询时可以完全的脱离样板化的代码。在有些时候,合适样板化代码来完成一些特定的查询功能也是非常有必要的。比如我们在查询中若想使用group进行分组,或是查询A数据表后,将数据填充到B实体上,再或者进行一些子查询。

条条大路通北京。综合查询的实现也是一样,没有最好的方法,只有最合适的方法。在实际的使用过程中,应该根据当前的业务需求,尝试选择出最合适的那个查询方式。

6. 总结

本文中先后介绍了3种不同的综合查询方法:

  • 最简单、直接的创建自定义的DAO
  • @Repository进行扩展,就像访问标准的查询方法一样调用综合查询方法
  • 使用Specification减少样板化的代码

同样,你可以打开文章顶部的github地址来获取一份与本文相同的且可运行的code demo。不止如此,在code demo中我们还提供了用于验证的单元测试代码。希望能对你有所帮助。