1. 简介
Spring Data JPA 提供了多种操作实体的方式,比如常见的方法名查询和自定义的JPQL 查询。但在某些场景下,这些方式不够灵活,我们需要更程序化的手段 —— 比如 Criteria API 或 QueryDSL。
✅ Criteria API 的优势在于:它支持类型安全的动态查询构建。配合 JPA 的 Metamodel,甚至能在编译期检查字段名和类型是否正确,避免拼写错误导致运行时异常。
❌ 但它的缺点也很明显:代码冗长,充斥着大量模板代码,写起来费劲,读起来费眼。
本篇将带你用 Criteria 查询实现自定义 DAO 逻辑,并重点展示 Spring 是如何帮我们简化这些样板代码的。目标不是炫技,而是解决实际开发中的动态查询问题,比如“根据用户输入的多个可选条件组合查询”。
⚠️ 提示:本文面向有一定 Spring Data JPA 经验的开发者,基础概念如
@Entity
、JpaRepository
不再赘述。
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 的标准流程,拆解如下:
- 获取
CriteriaBuilder
—— 用于构建查询的“工厂”。 - 创建
CriteriaQuery<Book>
—— 定义查询目标和返回类型。 - 设置查询根(
Root<Book>
)—— 指定从哪个实体开始查。 - 构建
Predicate
条件 —— 这里是作者名精确匹配 + 书名模糊匹配。 - 将条件通过
where()
应用到查询 —— 多个条件默认以AND
连接。 - 创建
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