首页 / Spring Boot 入门教程 / JPA 进阶:分页、排序与自定义查询

Spring Boot 入门教程

JPA 进阶:分页、排序与自定义查询

本教程共 48 篇 · 第 28 篇 · 更新于 2026-08-13 · 约 6 分钟阅读

Spring BootSpring Data JPA分页排序@QueryJPQL原生SQL

本节目标:会用 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

分页返回值有两种:PageSlicePage 会额外执行一条 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) 就是例子。@QueryPageable 参数同理,Spring Data 会自动拼上 limitorder by

JPQL 和 SQL 的区别再强调一次:JPQL 里写的是实体名和属性名Bookb.price),SQL 里写的是表名和列名booksprice)。实体名写错,启动或运行时直接报错,别拿表名往 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 的批更新,一次数据库交互,比逐条 findByIdsave 高效得多。

原生 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 直接序列化时自带 contenttotalElementstotalPages 等字段,前端解析方便。不想暴露内部字段,再包一层 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 版本化迁移。