Spring Data JPA 查询之道:方法命名与示例查询完全指南

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)

对于 findreadgetquery 这几个引导词,Spring Data JPA 的处理方式完全一致。delete...By 则是先执行查询再删除结果,是 CrudRepository.delete() 的快捷方式。

1.2 结果集控制:Top / First / Distinct

你可以在引导词和 By 之间插入 TopFirstDistinct 来控制查询结果:

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 findByFirstnamefindByFirstnameIsfindByFirstnameEquals ... 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)

特别说明 :对于 StartingWithEndingWithContaining 等包含 LIKE 的关键字,Spring Data JPA 会对参数中的通配符(%_)进行转义处理 ,使其仅作为字面值匹配,防止 SQL 注入风险。转义字符可通过 @EnableJpaRepositoriesescapeCharacter 属性配置。

1.4 IgnoreCase 与 OrderBy

除了上述关键字,还可以使用 IgnoreCaseOrderBy 来进一步细化查询:

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 由以下四个部分组成:

  1. Probe(探针):填充了属性值的领域对象实例,作为查询的"样本"。
  2. ExampleMatcher(匹配器):定义如何匹配特定字段的规则,可在多个 Example 中复用。
  3. Example(示例):由 Probe 和 ExampleMatcher 组合而成,用于创建实际的查询。
  4. 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 是理想选择;
  • 对于极其复杂 的查询(如多表关联、子查询、聚合函数),请使用 @QuerySpecification

结语

Spring Data JPA 的方法命名派生查询和按示例查询,分别从"声明式"和"示例式"两个维度,极大地简化了数据访问层的开发工作。前者让你以最少的代码量完成最常见的查询,后者则赋予你动态构建查询条件的灵活性。掌握这两种查询方式,足以应对日常开发中 80% 以上的查询场景。当需求超出它们的能力范围时,@QuerySpecification 会是你的有力补充。希望本文能帮助你在实际项目中游刃有余地运用 Spring Data JPA 的查询能力。

相关推荐
IT小盘10 小时前
12-Prompt不等于一句话-System-User-Context三层结构
java·windows·prompt
momo12 小时前
Java+WebAI黑马知识点
java·spring boot·mybatis
卷无止境12 小时前
PageIndex:把RAG的检索逻辑从"找相似"改成了"讲道理"
后端·python
卷无止境12 小时前
什么是VoxCPM2?一场TTS架构的彻底重构
后端·python
重庆小透明13 小时前
带你从不同视角了解三大MQ
java·学习·spring·kafka·rabbitmq·rocketmq
程序员cxuan13 小时前
A 社封杀了 1140 万个账号,下午 Claude 崩了。
人工智能·后端·程序员
冰心孤城13 小时前
C++ 与 C#混合编程 示例 (基于VS)
java·c++·c#
星栈14 小时前
MCP 2026 史上最大架构重构:从会话握手到无状态协议
人工智能·后端
IT_陈寒14 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端