《Spring Boot 入门》12.3 派生查询方法

本节把图书服务的查询需求变成方法名:讲清 findBy/readBy 等前缀与条件关键字的语法规则,给出 And、Between、Containing、In、OrderBy、IgnoreCase、Top 等关键字全表,说明属性路径导航与 @Query 的取舍,并用 @Modifying 实现更新删除、用投影接口只取需要的列。

本节目标:用派生查询把「按作者查、按年份区间查、按标题模糊查」这些需求写成方法名,掌握条件关键字全表、属性路径导航、@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 / Orand / orfindByTitleAndAuthor
Is / Equals=findByIsbnIs
Betweenbetween ? and ?findByPublishedYearBetween
LessThan / LessThanEqual< / <=findByPublishedYearLessThan
GreaterThan / GreaterThanEqual> / >=findByPublishedYearGreaterThan
Likelike ?(需自带 %)findByTitleLike
Containinglike %?%findByTitleContaining
StartingWithlike ?%findByTitleStartingWith
EndingWithlike %?findByTitleEndingWith
In / NotInin (?)findByAuthorIn(Collection<String>)
IsNull / NotNullis null / is not nullfindByIsbnIsNull
True / False= true / = falsefindByAvailableTrue
IgnoreCaselower(...) = lower(?)findByTitleIgnoreCase
OrderByorder byfindByAuthorOrderByTitleAsc
Distinctselect distinctfindDistinctByAuthor
Top / Firstlimit ?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

派生查询擅长「名字能读懂的简单条件」,但有几类需求它表达不了:

  1. 多表 JOIN 加复杂条件,方法名会长到不可读。
  2. 聚合:group by、having、avg、count 的复杂组合。
  3. 数据库专有语法:窗口函数、insert ... select。
  4. 更新/删除(见下一节)。

这时改用 @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 关联映射 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计