1. 简介

Spring Data JPA 提供了多种操作实体的方式,比如常见的方法名查询和自定义的JPQL 查询。但在某些场景下,这些方式不够灵活,我们需要更程序化的手段 —— 比如 Criteria APIQueryDSL

Criteria API 的优势在于:它支持类型安全的动态查询构建。配合 JPA 的 Metamodel,甚至能在编译期检查字段名和类型是否正确,避免拼写错误导致运行时异常。

❌ 但它的缺点也很明显:代码冗长,充斥着大量模板代码,写起来费劲,读起来费眼。

本篇将带你用 Criteria 查询实现自定义 DAO 逻辑,并重点展示 Spring 是如何帮我们简化这些样板代码的。目标不是炫技,而是解决实际开发中的动态查询问题,比如“根据用户输入的多个可选条件组合查询”。

⚠️ 提示:本文面向有一定 Spring Data JPA 经验的开发者,基础概念如 @EntityJpaRepository 不再赘述。

2. 示例应用

为了便于对比,我们统一实现同一个业务逻辑:根据作者名和书名关键词查询书籍

先看 Book 实体类:

@Entity
class Book {

    @Id
    Long id;
    String title;
    String author;

    // getters and setters

}

为简化示例,本文暂不使用 JPA Metamodel(即静态类型元模型),避免增加理解成本。实际项目中建议启用,能大幅提升类型安全性。

3. 使用 @Repository 类实现 Criteria 查询

在 Spring 的分层架构中,数据访问逻辑应放在被 @Repository 注解的类中。这类 Bean 会自动参与 Spring 的异常翻译机制(比如将 JPA 异常转为 Spring 的 DataAccessException),✅ 这是推荐做法。

实现 Criteria 查询的核心是 EntityManager,我们可以通过依赖注入获取:

@Repository
class BookDao {

    private final EntityManager em;

    public BookDao(EntityManager em) {
        this.em = em;
    }

    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 的标准流程,拆解如下:

  1. 获取 CriteriaBuilder —— 用于构建查询的“工厂”。
  2. 创建 CriteriaQuery<Book> —— 定义查询目标和返回类型。
  3. 设置查询根(Root<Book>)—— 指定从哪个实体开始查。
  4. 构建 Predicate 条件 —— 这里是作者名精确匹配 + 书名模糊匹配。
  5. 将条件通过 where() 应用到查询 —— 多个条件默认以 AND 连接。
  6. 创建 TypedQuery 并执行,返回结果列表。

⚠️ 注意:book.get("author") 中的字段名是字符串,存在拼错风险。这也是为什么推荐配合 Metamodel 使用(如 Book_.author)。

4. 为 Repository 接口扩展自定义方法

Spring Data JPA 的自动查询功能(如 findByAuthorAndTitle)非常方便,但面对复杂或动态条件时就无能为力了。

此时,我们可以:

  • ✅ 方式一:单独写一个 @Repository 类(如上节的 BookDao
  • ✅ 方式二:JpaRepository 接口扩展自定义方法,实现更无缝的集成

这就是 Spring Data 的 可组合仓库(Composable Repositories) 模式。

4.1 定义自定义接口

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

4.2 主 Repository 接口继承它

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

4.3 实现自定义逻辑

Spring 要求实现类命名为 XxxRepositoryImpl(Impl 后缀是约定):

@Repository
class BookRepositoryImpl implements BookRepositoryCustom {

    private final EntityManager em;

    public BookRepositoryImpl(EntityManager em) {
        this.em = em;
    }

    @Override
    public 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 && !authorName.isEmpty()) {
            predicates.add(cb.equal(book.get("author"), authorName));
        }
        if (title != null && !title.isEmpty()) {
            predicates.add(cb.like(book.get("title"), "%" + title + "%"));
        }
        // 动态组合条件
        if (!predicates.isEmpty()) {
            cq.where(predicates.toArray(new Predicate[0]));
        }

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

这样,当你注入 BookRepository 时,Spring 会自动把 BookRepositoryImpl 的方法织入进去,调用体验和原生方法一致。

💡 踩坑提醒:实现类必须叫 BookRepositoryImpl,且放在同一包下,否则 Spring 找不到。

5. 使用 JPA Specifications 简化动态查询

上面的方式虽然可行,但当条件越来越多时,if 嵌套会让代码变得难以维护。有没有更优雅的解法?

有!Spring Data JPA 提供了 Specification 接口,把查询条件封装成可复用、可组合的“规格”

5.1 Specification 接口定义

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

5.2 创建可复用的条件

我们可以静态方法形式定义常用条件:

public class BookSpecifications {

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

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

5.3 Repository 启用 Specification 支持

只需让接口继承 JpaSpecificationExecutor<Book>

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

5.4 组合查询,一行搞定

现在你可以像搭积木一样组合条件:

// 只查作者
bookRepository.findAll(hasAuthor("鲁迅"));

// 作者 + 书名关键词
bookRepository.findAll(
    Specification.where(hasAuthor("鲁迅"))
                 .and(titleContains("故乡"))
);

这种方式的优势:

  • 条件可复用,避免重复代码
  • 组合灵活,支持 and()or()not()
  • Spring 封装了 Criteria 的模板代码,你只需关注业务逻辑

⚠️ 但也有限制:

  • 仅适用于“查询实体列表”这类简单场景
  • ❌ 不支持 GROUP BY、自定义投影(返回 DTO)、子查询等复杂结构

🔧 如果你需要更复杂的查询,还是得回到 EntityManager + Criteria API 的手动方式。

6. 总结

本文介绍了在 Spring Data JPA 中使用 Criteria 查询的三种实践方式:

方式 适用场景 优点 缺点
@Repository 完全自定义逻辑 最灵活,无限制 模板代码多
自定义 Repository 方法 需与自动查询集成 接口统一,调用简洁 需遵守命名约定
Specification 动态条件组合 可复用、可组合,代码清爽 仅支持简单查询

📌 选型建议:

  • 条件固定 → 用方法名查询
  • 条件动态可选 → 用 Specification
  • 查询结构复杂(如分组、子查询)→ 手写 Criteria 或原生 SQL

所有示例代码已上传至 GitHub:https://github.com/yourname/spring-data-jpa-criteria-demo


原始标题:Use Criteria Queries in a Spring Data Application