JPA 进阶:分页、排序与自定义查询
本教程共 48 篇 · 第 28 篇 · 更新于 2026-08-13 · 约 6 分钟阅读
本节目标:会用 Pageable 做分页排序,用 @Query 写复杂查询,用 @Modifying 做更新删除,并分清 JPQL 与原生 SQL。
为什么需要分页
数据量一大,全查出来就是灾难。一万本书一次 findAll(),内存和网络都受不了。分页是标配:每次只取一页,比如每页 10 条。
分页还有个隐藏好处:响应时间稳定。数据从一万涨到十万,单页查询的耗时基本不变,接口不会越跑越慢。
Spring Data 的 Pageable 就是干这个的。它封装了页码、每页大小、排序规则。
分页与排序的基本用法
先给 Repository 加一个分页查询方法。其实不用加,JpaRepository 自带 findAll(Pageable):
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface BookRepository extends JpaRepository<Book, Long> {
// 按书名模糊查询,支持分页
Page<Book> findByTitleContaining(String keyword, Pageable pageable);
}
服务层构造 Pageable 并调用:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public Page<Book> findBooks(int page, int size, String sortBy, String direction) {
Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
return bookRepository.findAll(pageable);
}
}
PageRequest.of(page, size, sort) 三个参数:页码(从 0 开始)、每页条数、排序规则。初学者在这里容易懵,前端传第 1 页,后端对应 page=0。
Sort.by(Direction.DESC, "price") 表示按价格降序。多个字段排序就多传几个属性名:
// 先按价格降序,价格相同再按书名升序
Sort sort = Sort.by(
Sort.Order.desc("price"),
Sort.Order.asc("title"));
Pageable pageable = PageRequest.of(0, 10, sort);
方向不传默认是升序。分页参数从请求来的时候,注意校验:页码不能为负,每页条数要限个上限(比如 100),不然用户传个 10000 就能拖垮数据库。
控制器里接分页参数的完整写法:
@RestController
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/books")
public Page<Book> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "ASC") String direction) {
return bookService.findBooks(page, size, sortBy, direction);
}
}
请求 GET /books?page=0&size=10&sortBy=price&direction=DESC,就能拿到按价格降序的第一页。参数带默认值,不传也能跑,接口更宽容。
Page 返回了什么
Page<Book> 不只是数据列表,还带分页元信息:
Page<Book> result = bookService.findBooks(0, 10, "price", "DESC");
List<Book> books = result.getContent(); // 当前页的数据
int pageNumber = result.getNumber(); // 当前页码
int totalPages = result.getTotalPages(); // 总页数
long totalElements = result.getTotalElements(); // 总记录数
boolean hasNext = result.hasNext(); // 还有没有下一页
前端做分页组件,这几个值全用得上。控制器里直接返回 Page,Jackson 会自动序列化成 JSON。
Note分页返回值有两种:
Page和Slice。Page会额外执行一条 count 查询统计总数,数据量大时有点开销;Slice只告诉你「有没有下一页」,不查总数,性能更好。列表页需要页码组件用Page,无限滚动这类场景用Slice更合适。
@Query:写自己的查询
方法名能表达的条件有限。@Query 注解可以写 JPQL——JPA 的查询语言,语法像 SQL,但操作的是实体和属性,不是表和列:
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface BookRepository extends JpaRepository<Book, Long> {
// JPQL:操作 Book 实体,别名 b 随意,:minPrice 是命名参数
@Query("select b from Book b where b.price >= :minPrice order by b.price desc")
List<Book> findExpensiveBooks(@Param("minPrice") BigDecimal minPrice);
// 只查某一列,返回 String
@Query("select b.title from Book b where b.id = :id")
String findTitleById(@Param("id") Long id);
}
@Param("minPrice") 把方法参数绑定到 JPQL 里的 :minPrice。这样写的好处:编译期就能校验属性名,数据库换了也能跑(JPQL 与数据库无关)。
派生的方法名查询和 @Query 怎么选?简单条件用方法名,一行搞定;条件复杂、涉及多表或不想让方法名长成天书,用 @Query。两条路最终都会生成 SQL,没有性能差别。
方法名派生查询同样支持分页排序——只要在方法里加一个 Pageable 参数,上一节的 findByTitleContaining(keyword, pageable) 就是例子。@Query 加 Pageable 参数同理,Spring Data 会自动拼上 limit 和 order by。
JPQL 和 SQL 的区别再强调一次:JPQL 里写的是实体名和属性名(Book、b.price),SQL 里写的是表名和列名(books、price)。实体名写错,启动或运行时直接报错,别拿表名往 JPQL 里套。
JPQL 查询带分页时,count 查询一般会自动生成;用了 join fetch 这类复杂写法,可能得手动指定 countQuery:
@Query(value = "select b from Book b where b.price >= :minPrice",
countQuery = "select count(b) from Book b where b.price >= :minPrice")
Page<Book> findExpensiveBooks(@Param("minPrice") BigDecimal minPrice, Pageable pageable);
更新和删除:别忘了 @Modifying
@Query 默认只支持查询。写 update/delete 的 JPQL 必须加 @Modifying,否则启动报错:
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface BookRepository extends JpaRepository<Book, Long> {
@Modifying
@Query("update Book b set b.price = :price where b.id = :id")
int updatePrice(@Param("id") Long id, @Param("price") BigDecimal price);
@Modifying
@Query("delete from Book b where b.publishDate < :date")
int deleteOldBooks(@Param("date") LocalDate date);
}
方法返回 int,表示受影响的行数。注意两点:
@Modifying的更新需要事务。在 Service 层方法上加@Transactional,否则抛TransactionRequiredException。- 修改操作默认不会清空一级缓存,同一个事务里再查可能拿到旧数据。加
@Modifying(clearAutomatically = true)可以强制清缓存。
@Modifying 适合按条件批量改。它走的是 JPQL 翻译成 SQL 的批更新,一次数据库交互,比逐条 findById 再 save 高效得多。
原生 SQL:绕过 JPQL
JPQL 搞不定数据库特有语法时,用原生 SQL。加 nativeQuery = true,写的就是数据库方言:
public interface BookRepository extends JpaRepository<Book, Long> {
// 原生 SQL:操作真实的表 books
@Query(value = "select * from books where price < :max", nativeQuery = true)
List<Book> findCheapBooksNative(@Param("max") BigDecimal max);
// 原生 SQL 也支持分页,countQuery 用来算总数
@Query(value = "select * from books where title like :keyword",
countQuery = "select count(*) from books where title like :keyword",
nativeQuery = true)
Page<Book> searchNative(@Param("keyword") String keyword, Pageable pageable);
}
Warning原生 SQL 与数据库强绑定,换数据库就要改。能用 JPQL 尽量用 JPQL;只有窗口函数、全文索引这类特性才值得上原生 SQL。
原生 SQL 的正确使用姿势是「局部使用」:哪条查询需要数据库特性,就在哪条上开 nativeQuery,其余保持 JPQL。项目里原生 SQL 的比例是重要的健康指标,越高说明越依赖特定数据库,迁移成本越大。
组合使用:排序 + 分页 + 自定义查询
一个真实场景全用上:搜索书名、按价格排序、分页返回。
@Query("select b from Book b where b.title like %:keyword%")
Page<Book> search(@Param("keyword") String keyword, Pageable pageable);
// 服务层调用
Page<Book> result = bookRepository.search(
"Spring",
PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "price")));
like %:keyword% 里的 % 写在 JPQL 中,参数只传关键字本身。
分页大小建议给默认值并限制最大值。用户传个 size=10000 的场景,实际开发里很常见,接口要扛得住。
排序字段最好做白名单校验:只允许按实体里真实存在的字段排,防止用户传任意字段名触发数据库报错。
Page 直接序列化时自带 content、totalElements、totalPages 等字段,前端解析方便。不想暴露内部字段,再包一层 DTO 返回。
Tip复杂的动态条件(可选参数、多条件组合)用方法名和 @Query 都会很别扭,那是 Specifications 或 Query by Example 的主场,入门阶段先记住它们的存在即可。
一张表理清四种查询方式
| 方式 | 写法 | 适合场景 |
|---|---|---|
| 方法名派生 | findByTitleAndPriceLessThan | 条件少、固定 |
| @Query JPQL | @Query("select b from Book b where b.price > :min") | 条件复杂、多表 |
| @Query 原生 | nativeQuery = true | 数据库特性 |
| Specifications | 编程式拼接条件 | 动态条件组合 |
前三种本节都写过代码,第四种留个印象,后面用到再深入。
小结
分页用 PageRequest.of(page, size, sort),排序用 Sort.by,返回 Page 自带元信息。复杂查询用 @Query 写 JPQL,更新删除加 @Modifying 并配事务,数据库特性才上原生 SQL。
下一节看数据库结构怎么管理:Flyway 版本化迁移。