本节目标:用派生查询把「按作者查、按年份区间查、按标题模糊查」这些需求写成方法名,掌握条件关键字全表、属性路径导航、
@Query与@Modifying的用法,并认清属性名拼错会在启动期报错。
适用版本:Spring Boot 4.1.x(Java 21)
12.3 派生查询方法
上一节的 BookRepository 只会 save 和 findById。真实需求远不止这些:「列出某作者的全部图书」「找出 2020 年后出版的」「按标题模糊搜」。如果每种查询都自己写实现,仓库很快就会膨胀成几百行样板代码。
Spring Data 的解法是派生查询(derived query):你按约定给方法起名,它解析方法名、生成对应的查询。本节把图书服务的查询需求逐个落地。
12.3.1 一条方法名是怎么变成 SQL 的
派生查询的方法名由四段拼成:
<前缀>By<属性>[条件关键字]<OrderBy><排序属性><排序方向>
例如:
List<Book> findByAuthorAndPublishedYearGreaterThanOrderByTitleAsc(
String author, int year);
Spring Data 在启动时解析这个名字,生成大致如下的 JPQL:
select b from Book b
where b.author = ?1 and b.publishedYear > ?2
order by b.title asc
前缀有四个同义写法:findBy、readBy、queryBy、getBy。它们生成的查询完全一样,选一个读起来顺口的即可;findBy 最常用。By 后面才是真正的表达式,必须和实体属性名严格对应——这一点后面会看到它如何变成启动期的报错。
12.3.2 条件关键字全表
把关键字当成积木,条件就能自由组合。下表覆盖最常用的一批:
| 关键字 | 生成的 SQL 片段 | 方法名示例 |
|---|---|---|
And / Or | and / or | findByTitleAndAuthor |
Is / Equals | = | findByIsbnIs |
Between | between ? and ? | findByPublishedYearBetween |
LessThan / LessThanEqual | < / <= | findByPublishedYearLessThan |
GreaterThan / GreaterThanEqual | > / >= | findByPublishedYearGreaterThan |
Like | like ?(需自带 %) | findByTitleLike |
Containing | like %?% | findByTitleContaining |
StartingWith | like ?% | findByTitleStartingWith |
EndingWith | like %? | findByTitleEndingWith |
In / NotIn | in (?) | findByAuthorIn(Collection<String>) |
IsNull / NotNull | is null / is not null | findByIsbnIsNull |
True / False | = true / = false | findByAvailableTrue |
IgnoreCase | lower(...) = lower(?) | findByTitleIgnoreCase |
OrderBy | order by | findByAuthorOrderByTitleAsc |
Distinct | select distinct | findDistinctByAuthor |
Top / First | limit ? | findTop3ByOrderByPublishedYearDesc |
几个使用要点:
Like和Containing别混。Like要你自己在参数里带通配符("%Java%"),Containing会自动包上%,传纯文本即可。日常模糊搜索用Containing。IgnoreCase可以叠加:findByTitleContainingIgnoreCase(String keyword)就是「忽略大小写的模糊匹配」。Top/First后面可跟数字:findFirstBy...等价于findTop1By...。In接收集合:参数类型是Collection<String>、List<String>或数组。
12.3.3 属性路径导航
By 后面的属性可以跨关联点进去。假设 Book 有一个 Author 关联,Author 上有 name:
// 下划线显式分隔属性路径(推荐,歧义最小)
List<Book> findByAuthor_Name(String name);
// 驼峰直接连写,Spring Data 会尝试解析
List<Book> findByAuthorName(String name);
两种写法等价,但遇到有歧义的名字时用下划线更保险。例如 findByAuthorName,解析器可能理解成「关联 author 的 name」,也可能理解成「属性 authorName」。下划线明确告诉它「author 是路径、name 是终点属性」。跨关联查询本章只做铺垫,完整的关联映射在下一章展开。
12.3.4 什么时候该用 @Query
派生查询擅长「名字能读懂的简单条件」,但有几类需求它表达不了:
- 多表 JOIN 加复杂条件,方法名会长到不可读。
- 聚合:
group by、having、avg、count的复杂组合。 - 数据库专有语法:窗口函数、
insert ... select。 - 更新/删除(见下一节)。
这时改用 @Query 写 JPQL:
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface BookRepository extends JpaRepository<Book, Long> {
@Query("select b from Book b where b.publishedYear > :year order by b.title")
List<Book> findPublishedAfter(@Param("year") int year);
}
判断标准很简单:方法名能一眼读懂就用派生,读起来像绕口令就换 @Query。不要为了「不写 SQL」硬凑方法名——findByAuthorAndPublishedYearGreaterThanAndIsbnIsNotNullOrderByTitleAsc 这种名字,维护者宁可看 JPQL。
12.3.5 @Modifying 做更新与删除
@Query 默认只做查询。要做更新或删除,必须加 @Modifying:
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.transaction.annotation.Transactional;
@Modifying
@Transactional
@Query("update Book b set b.author = :author where b.isbn = :isbn")
int updateAuthorByIsbn(@Param("isbn") String isbn, @Param("author") String author);
三个必须注意的点:
- 要事务。
@Modifying查询必须在事务里执行,通常放在@Transactional的服务方法上,或像上面直接标在仓库方法上。 - 返回
int是影响行数,不是实体。 - 它绕过持久化上下文。批量更新直接打数据库,内存里已经加载的实体不会同步,可能读到旧值。需要同步时加
@Modifying(clearAutomatically = true, flushAutomatically = true)。
删除同理,用 delete from Book b where ...。不过简单的删除/计数/存在判断,Spring Data 还提供了派生写法,不必写 JPQL:
long countByAuthor(String author);
boolean existsByIsbn(String isbn);
void deleteByIsbn(String isbn);
12.3.6 返回类型怎么选
同一个查询,返回类型不同,行为和性能也不同:
| 返回类型 | 语义 | 适用 |
|---|---|---|
Book | 单个结果,多于一条抛 NonUniqueResultException | 按唯一键查 |
Optional<Book> | 单个结果,可能为空 | 推荐,强制调用方处理「查不到」 |
List<Book> | 多条结果 | 列表查询 |
Stream<Book> | 流式读取 | 大数据集逐条处理(需事务包裹) |
Page<Book> | 分页 + 总数 | 分页列表,需传 Pageable |
Slice<Book> | 分页,不查总数 | 无限滚动 |
BookTitle(投影接口) | 只取部分列 | 减少数据传输 |
投影接口是常被忽略的省流利器。只想要标题和作者时:
public interface BookSummary {
String getTitle();
String getAuthor();
}
// 仓库里
List<BookSummary> findByPublishedYearGreaterThan(int year);
Spring Data 会生成只 select 这两列的查询,而不是把整行读出来。
12.3.7 常见错误与启动期校验
派生查询最大的优点是启动期校验:方法名解析不了,应用直接起不来,而不是等到运行时才炸。最常见的两类错误:
第一类,属性名拼错。 把 title 写成 titel:
Caused by: org.springframework.data.mapping.PropertyReferenceException:
No property 'titel' found for type 'Book'; Did you mean 'title'?
注意那句 Did you mean 'title'?——Spring Data 会给出最接近的候选,照着改就行。这类错误只影响带该方法的仓库,但会导致整个应用启动失败。
第二类,返回类型不匹配。 例如 isbn 有唯一约束,你写 Book findByIsbn(...) 没问题;但若字段不唯一,运行时会抛 NonUniqueResultException。又如把分页方法写成 List<Book> findAll(Pageable pageable),编译期就过不去——分页必须返回 Page 或 Slice。凡是可能查不到或可能多条,就用 Optional / List,别用裸实体。
还有一种「不报错但危险」的情况:Like 忘了带 %,结果只做精确匹配,测试数据恰好命中,上线才发现搜不出东西。写模糊查询优先用 Containing。
12.3.8 把图书仓库补齐
把本节的方法合起来,BookRepository 长这样:
package com.example.bookstore.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import com.example.bookstore.domain.Book;
public interface BookRepository extends JpaRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn);
List<Book> findByAuthorContainingIgnoreCase(String author);
List<Book> findByPublishedYearBetweenOrderByPublishedYearDesc(int from, int to);
List<Book> findTop5ByOrderByPublishedYearDesc();
long countByAuthor(String author);
boolean existsByIsbn(String isbn);
List<BookSummary> findByPublishedYearGreaterThan(int year);
@Modifying
@Transactional
@Query("update Book b set b.author = :author where b.isbn = :isbn")
int updateAuthorByIsbn(@Param("isbn") String isbn, @Param("author") String author);
}
启动应用时,Spring Data 会为这些方法逐一生成查询;只要有一个方法名不合规,控制台就会在启动阶段报出 PropertyReferenceException,把问题拦在上线之前。
小结
派生查询把「写查询」变成了「起方法名」,本节要点:
- 方法名结构是
<前缀>By<属性>[条件]<OrderBy><排序>,前缀findBy/readBy/queryBy/getBy等价。 - 条件关键字可自由组合,日常最常用
And、Between、Containing、In、OrderBy、IgnoreCase、Top。 - 跨关联用属性路径导航,有歧义时用下划线(
findByAuthor_Name)。 - 方法名读不懂就换
@Query;更新删除必须@Modifying+ 事务。 - 返回类型优先
Optional/List,只取部分列用投影接口;属性名拼错会在启动期报PropertyReferenceException。
到这里,图书服务已经能用方法名完成大部分查询。但当 Book 真正关联到 Author、Publisher 时,如何映射一对多、多对多,以及如何控制加载策略,是下一章的主题。
阅读导航:上一节:12.2 实体与 Repository · 下一节:13.1 关联映射 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。