Spring Data JPA 查询之道:方法命名与示例查询完全指南
在 Spring Data JPA 中,查询的编写方式多种多样,而其中最优雅、最符合"约定优于配置"理念的,莫过于方法命名派生查询(Derived Query Methods) 和按示例查询(Query by Example,QBE) 两种方式。前者让你只需按照既定规则命名方法,框架便能自动生成 SQL;后者则让你通过一个"样本对象"来动态构建查询条件。本文将全面、深入地介绍这两种查询方式,助你从容应对日常开发中的各类查询需求。
一、方法命名派生查询(Derived Query Methods)
方法命名派生查询是 Spring Data JPA 最核心、最常用的查询方式。开发者只需在 Repository 接口中按照约定的命名规则定义方法,Spring Data JPA 就能在运行时自动解析方法名并生成对应的 JPQL 查询语句。这意味着,对于大量常见的查询场景,你完全不需要编写任何 JPQL 或 SQL。
1.1 命名结构
一个派生查询方法的命名由两部分组成,中间以 By 分隔:
- 引导词(Introducer) :方法名的起始部分,定义查询的主题(即要执行什么操作)。
- 条件部分(Criteria) :
By之后的部分,定义查询的谓词(即查询条件)。
arduino
List<User> findByName(String name);
─┬─ ──┬──
引导词 条件部分
支持的引导词
Spring Data JPA 支持多种引导词,它们在功能上基本等价:
| 引导词 | 含义 | 示例 |
|---|---|---|
find...By |
返回匹配的实体或集合 | findByName(String name) |
read...By |
同 find |
readByName(String name) |
get...By |
同 find |
getByName(String name) |
query...By |
同 find |
queryByName(String name) |
count...By |
返回匹配结果的数量 | countByName(String name) |
exists...By |
判断是否存在匹配结果 | existsByName(String name) |
delete...By |
删除匹配的实体 | deleteByName(String name) |
对于 find、read、get、query 这几个引导词,Spring Data JPA 的处理方式完全一致。delete...By 则是先执行查询再删除结果,是 CrudRepository.delete() 的快捷方式。
1.2 结果集控制:Top / First / Distinct
你可以在引导词和 By 之间插入 Top、First 或 Distinct 来控制查询结果:
java
// 返回年龄最大的前3条记录
List<User> findTop3ByAge();
// 返回年龄最大的第1条记录(等价于 Top1)
User findFirstByAge();
// 去重查询
List<User> findDistinctByLastNameAndFirstName(String lastName, String firstName);
1.3 查询条件关键字
条件部分是方法命名的核心,由属性名 和关键字组合而成。下表列出了 JPA 支持的所有查询关键字及其对应的 JPQL 片段:
| 关键字 | 示例方法 | JPQL 片段 |
|---|---|---|
And |
findByLastnameAndFirstname |
... where x.lastname = ?1 and x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
... where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals |
findByFirstname、findByFirstnameIs、findByFirstnameEquals |
... where x.firstname = ?1 |
Between |
findByStartDateBetween |
... where x.startDate between ?1 and ?2 |
LessThan |
findByAgeLessThan |
... where x.age < ?1 |
LessThanEqual |
findByAgeLessThanEqual |
... where x.age <= ?1 |
GreaterThan |
findByAgeGreaterThan |
... where x.age > ?1 |
GreaterThanEqual |
findByAgeGreaterThanEqual |
... where x.age >= ?1 |
After |
findByStartDateAfter |
... where x.startDate > ?1 |
Before |
findByStartDateBefore |
... where x.startDate < ?1 |
IsNull, Null |
findByAgeIsNull |
... where x.age is null |
IsNotNull, NotNull |
findByAgeIsNotNull |
... where x.age not null |
Like |
findByFirstnameLike |
... where x.firstname like ?1 |
NotLike |
findByFirstnameNotLike |
... where x.firstname not like ?1 |
StartingWith |
findByFirstnameStartingWith |
... where x.firstname like ?1(参数追加 %) |
EndingWith |
findByFirstnameEndingWith |
... where x.firstname like ?1(参数前置 %) |
Containing |
findByFirstnameContaining |
... where x.firstname like ?1(参数前后加 %) |
NotContaining |
findByFirstnameNotContaining |
... where x.firstname not like ?1 |
In |
findByAgeIn |
... where x.age in ?1 |
NotIn |
findByAgeNotIn |
... where x.age not in ?1 |
True |
findByActiveTrue |
... where x.active = true |
False |
findByActiveFalse |
... where x.active = false |
IgnoreCase |
findByFirstnameIgnoreCase |
... where UPPER(x.firstname) = UPPER(?1) |
特别说明 :对于
StartingWith、EndingWith、Containing等包含LIKE的关键字,Spring Data JPA 会对参数中的通配符(%、_)进行转义处理 ,使其仅作为字面值匹配,防止 SQL 注入风险。转义字符可通过@EnableJpaRepositories的escapeCharacter属性配置。
1.4 IgnoreCase 与 OrderBy
除了上述关键字,还可以使用 IgnoreCase 和 OrderBy 来进一步细化查询:
java
// 单属性忽略大小写
List<User> findByLastnameIgnoreCase(String lastname);
// 所有属性忽略大小写
List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// 静态排序
List<User> findByLastnameOrderByFirstnameAsc(String lastname);
List<User> findByLastnameOrderByFirstnameDesc(String lastname);
1.5 属性表达式与嵌套属性
条件部分可以引用嵌套属性,只需用下划线 _ 连接即可:
java
// 假设 User 有一个 Address 类型的 address 属性,Address 有 city 属性
List<User> findByAddressCity(String city);
Spring Data JPA 会遍历属性路径,生成相应的 JOIN 查询。如果属性名含有下划线(如 first_name),JPA 会优先将其视为属性路径的一部分进行解析。
1.6 完整示例
实体类:
java
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Integer id;
private String name;
private Integer age;
private ZonedDateTime birthDate;
private Boolean active;
// getters and setters
}
Repository 接口:
java
public interface UserRepository extends JpaRepository<User, Integer> {
// 等值查询
List<User> findByName(String name);
List<User> findByNameIs(String name);
List<User> findByNameEquals(String name);
List<User> findByNameIsNot(String name);
// null 判断
List<User> findByNameIsNull();
List<User> findByNameIsNotNull();
// 布尔值
List<User> findByActiveTrue();
List<User> findByActiveFalse();
// 模糊匹配
List<User> findByNameStartingWith(String prefix);
List<User> findByNameEndingWith(String suffix);
List<User> findByNameContaining(String infix);
// 比较操作
List<User> findByAgeLessThan(int age);
List<User> findByAgeBetween(int from, int to);
// 组合条件
List<User> findByNameAndAge(String name, int age);
List<User> findByNameOrAge(String name, int age);
// 排序与限制
List<User> findTop3ByAgeOrderByBirthDateDesc(int age);
// 计数与存在判断
long countByName(String name);
boolean existsByName(String name);
}
1.7 何时使用 @Query
尽管方法命名派生查询非常强大,但在以下场景中,你可能需要改用 @Query 注解来手动编写 JPQL:
- 查询条件过于复杂,方法名会变得冗长难读;
- 需要使用 JPA 不支持的 SQL 特性;
- 需要执行更新或删除操作(配合
@Modifying); - 需要使用原生 SQL(
nativeQuery = true)。
二、按示例查询(Query by Example,QBE)
按示例查询(QBE)是 Spring Data JPA 从 1.10 版本开始支持的一种查询方式。它允许你通过一个样本实体对象(Probe) 来动态创建查询,无需编写任何包含字段名称的查询语句。
2.1 核心概念
QBE API 由以下四个部分组成:
- Probe(探针):填充了属性值的领域对象实例,作为查询的"样本"。
- ExampleMatcher(匹配器):定义如何匹配特定字段的规则,可在多个 Example 中复用。
- Example(示例):由 Probe 和 ExampleMatcher 组合而成,用于创建实际的查询。
- FetchableFluentQuery(流式查询):提供流式 API,允许对查询结果进行排序、投影等进一步定制。
2.2 基本用法
第一步:让 Repository 继承 QueryByExampleExecutor
JpaRepository 本身已经继承了 QueryByExampleExecutor,所以如果你的 Repository 继承了 JpaRepository,就已经具备了 QBE 能力。
java
public interface UserRepository extends JpaRepository<User, Integer> {
// 无需额外声明任何方法
}
第二步:创建 Probe 和 Example
java
User probe = new User();
probe.setName("张三");
probe.setAge(25);
Example<User> example = Example.of(probe);
List<User> results = userRepository.findAll(example);
默认行为下,字段值为 null 的属性会被忽略,字符串采用存储特定的默认匹配方式(通常为精确匹配)。
2.3 QueryByExampleExecutor 提供的方法
QueryByExampleExecutor 接口提供了以下核心方法:
java
public interface QueryByExampleExecutor<T> {
// 查找单个匹配的实体
<S extends T> Optional<S> findOne(Example<S> example);
// 查找所有匹配的实体
<S extends T> Iterable<S> findAll(Example<S> example);
// 查找所有匹配的实体(带排序)
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
// 分页查找
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
// 统计匹配数量
<S extends T> long count(Example<S> example);
// 判断是否存在匹配
<S extends T> boolean exists(Example<S> example);
}
2.4 ExampleMatcher:精细控制匹配规则
ExampleMatcher 让你可以对匹配行为进行精细控制:
2.4.1 创建 ExampleMatcher
java
// 默认:所有值都必须匹配(AND 逻辑)
ExampleMatcher matcher = ExampleMatcher.matching();
// 任意值匹配(OR 逻辑)
ExampleMatcher matcher = ExampleMatcher.matchingAny();
2.4.2 常用配置方法
java
ExampleMatcher matcher = ExampleMatcher.matching()
// 忽略指定字段(不参与查询)
.withIgnorePaths("id", "createdAt")
// 包含 null 值字段(默认忽略 null)
.withIncludeNullValues()
// 设置全局字符串匹配策略:后缀匹配(ENDNG_WITH)
.withStringMatcher(StringMatcher.ENDING)
// 设置全局字符串匹配策略:包含匹配(CONTAINING)
.withStringMatcher(StringMatcher.CONTAINING)
// 忽略大小写(全局)
.withIgnoreCase()
// 针对特定字段配置匹配规则
.withMatcher("name", match -> match.contains().ignoreCase())
.withMatcher("email", match -> match.startsWith());
2.4.3 字符串匹配策略
StringMatcher 枚举提供了以下策略:
| 策略 | 说明 | SQL 效果 |
|---|---|---|
DEFAULT |
存储特定默认值(通常为精确匹配) | = ? |
EXACT |
精确匹配 | = ? |
STARTING |
前缀匹配 | LIKE ?% |
ENDING |
后缀匹配 | LIKE %? |
CONTAINING |
包含匹配 | LIKE %?% |
2.4.4 完整示例
java
// 创建探针
User probe = new User();
probe.setName("张");
probe.setEmail("example");
probe.setActive(true);
// 创建匹配器
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("id", "createdAt", "updatedAt") // 忽略这些字段
.withStringMatcher(StringMatcher.CONTAINING) // 全局包含匹配
.withIgnoreCase() // 全局忽略大小写
.withMatcher("email", match -> match.endsWith()) // email 字段使用后缀匹配
.withMatcher("name", match -> match.startsWith().ignoreCase()); // name 字段前缀匹配 + 忽略大小写
// 创建 Example 并执行查询
Example<User> example = Example.of(probe, matcher);
List<User> results = userRepository.findAll(example);
2.5 FetchableFluentQuery:流式查询
FetchableFluentQuery 提供了更流畅的查询构建方式,支持排序、投影和结果处理:
java
User probe = new User();
probe.setActive(true);
Example<User> example = Example.of(probe);
// 链式调用:排序 + 分页 + 投影
List<User> results = userRepository.findBy(example,
query -> query
.sortBy(Sort.by("age").descending())
.page(PageRequest.of(0, 10))
.stream()
.collect(Collectors.toList())
);
2.6 QBE 的适用场景与局限性
适用场景:
- 查询条件动态变化,需要灵活组合;
- 领域对象频繁重构,不希望因字段名变更而修改查询代码;
- 希望编写与底层数据存储无关的查询代码。
局限性:
- 不支持嵌套或分组属性约束(如
firstname = ?0 or (firstname = ?1 and lastname = ?2)); - 不支持集合(
Collection)或 Map 类型的匹配; - 字符串匹配能力受底层数据库限制;
- 同一字段只能有一个过滤值,无法同时传入多个值。
三、方法命名查询 vs. 按示例查询:如何选择?
| 对比维度 | 方法命名派生查询 | 按示例查询(QBE) |
|---|---|---|
| 代码量 | 极简,只需声明方法 | 需要构建 Probe 和 Matcher |
| 可读性 | 方法名即文档,一目了然 | 需要查看代码才能理解查询条件 |
| 动态性 | 条件固定,难以应对动态场景 | 天然支持动态条件组合 |
| 灵活性 | 受限于关键字集 | 匹配规则可精细控制 |
| 类型安全 | 属性名在编译时无检查 | 属性名在编译时无检查 |
| 复杂度 | 简单查询最佳 | 中等复杂度查询的利器 |
| 重构友好 | 属性名变更需要同步修改方法名 | 只需修改 Probe 的 setter 调用 |
建议:
- 对于条件固定、语义清晰的查询,优先使用方法命名派生查询;
- 对于条件动态变化(如多字段组合搜索、可选过滤条件),QBE 是理想选择;
- 对于极其复杂 的查询(如多表关联、子查询、聚合函数),请使用
@Query或Specification。
结语
Spring Data JPA 的方法命名派生查询和按示例查询,分别从"声明式"和"示例式"两个维度,极大地简化了数据访问层的开发工作。前者让你以最少的代码量完成最常见的查询,后者则赋予你动态构建查询条件的灵活性。掌握这两种查询方式,足以应对日常开发中 80% 以上的查询场景。当需求超出它们的能力范围时,@Query 和 Specification 会是你的有力补充。希望本文能帮助你在实际项目中游刃有余地运用 Spring Data JPA 的查询能力。