国庆节10月1日
--


分页查询的两种实现——手写 count 与 limit 的原始方式,和 MyBatis 分页插件 PageHelper 的一行代码。逐层对比两者的三层代码,并用本机实测的两组 SQL 日志(自己写的 count(*) 对插件生成的 count(0))讲清 PageHelper 的实现机制与两条注意事项
.webp)
66 篇把分页”要传什么、要还什么、SQL 长什么样”分析清楚了,这一篇(PPT 第 46-53 页)动手写代码。PPT 把分页查询分成了两条路:
| 实现方式 | 一句话 | 对应 PPT 页 |
|---|---|---|
| 原始方式 | 自己写 count 的 SQL、自己算起始索引、自己在 SQL 里写 limit | 46-48 |
| PageHelper 分页插件 | 引一个插件,业务层加一行,count 和 limit 都由它自动生成 | 50-53 |
PPT 第 46 页是”原始方式”的小节封面,第 49 页是”分页查询”这一格的目录页(原始方式 / PageHelper 分页插件 两个词)——顺序也说明了学习顺序:先把原始方式写明白,才知道 PageHelper 到底替我们省了什么。
PPT 第 47 页先不写代码,而是把分页查询拆到三层上,并且直接把请求样例写在旁边:/emps?page=1&pageSize=10。
| 层 | 职责(PPT 原文) |
|---|---|
| Controller | 接收参数(分页);调用 Service 进行分页查询、获取 PageResult;响应结果 |
| Service | 调用 Mapper 接口查询总记录数;调用 Mapper 接口查询结果列表;封装 PageResult 对象并返回 |
| Mapper | 两条 SQL:① select count(*) ... ② select ... limit ... |
对照 66 篇的分析结果,这张表其实就是把”前端传 page/pageSize → 后端返回 total/rows”翻译成代码分工:
page、pageSize 收下来);PageResult);注意 Service 那一行”调用 Mapper 接口查询总记录数”——分页查询要调两次 Mapper,这是原始方式最明显的特征。
PPT 第 48 页把三层代码一起给了出来。一层一层看。
Controller:接收分页参数
1@GetMapping2public Result page(@RequestParam(defaultValue = "1") Integer page,3 @RequestParam(defaultValue = "10") Integer pageSize) {4 log.info("分页请求参数: {}, {}", page, pageSize);5 PageResult<Emp> pageResult = empService.page(page, pageSize);6 return Result.success(pageResult);7}PPT 在这一页的角落里专门提了一句:
通过
@RequestParam注解的defaultValue属性可以设置参数的默认值。
这就是”不传 page 就是第 1 页、不传 pageSize 就是每页 10 条”的实现方式——接口文档里 page/pageSize 的默认值(1 和 10)就落在这两行上。另外 log.info("分页请求参数: {}, {}", page, pageSize) 是 63 篇的日志写法:{} 是占位符,两个 {} 按顺序被 page、pageSize 替换——联调时第一眼就看这行日志,能立刻确认”参数到底传没传进来”。
Service:查两次、算一次、封装一次
1@Override2public PageResult<Emp> page(Integer page, Integer pageSize) {3 //1. 获取总记录数 total4 Long total = empMapper.count();5
6 //2. 获取分页查询结果列表 rows7 //2.1 计算起始索引8 Integer start = (page - 1) * pageSize;9 //2.2 执行查询10 List<Emp> empList = empMapper.list(start, pageSize);11
12 //3. 封装分页结果13 return new PageResult<>(total, empList);14}三件事,正好对应 66 篇的两句 SQL 加一个换算公式:
count() 拿总记录数(对应 select count(*) …);(page - 1) * pageSize 把页码换算成 起始索引(48 篇的公式),再调 list(start, pageSize) 拿当前页数据(对应带 limit 的那句 SQL);new PageResult<>(total, empList) 把两个值装起来(66 篇写的 @AllArgsConstructor 就是为这一行准备的)。Mapper:两条 SQL
1@Mapper2public interface EmpMapper {3
4 /**5 * 查询员工信息6 */7 @Select("select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id limit #{start},#{pageSize}")8 public List<Emp> list(Integer start, Integer pageSize);9
10 /**11 * 获取符合条件的总记录数12 */13 @Select("select count(*) from emp left join dept on emp.dept_id = dept.id")14 Long count();15}两点:
list 方法的 SQL 里自己写了 limit #{start},#{pageSize},所以方法要传两个参数(起始索引、每页记录数);dept.name as dept_name 用的是”下划线”别名——58 篇讲过,只要 yml 里开着驼峰开关(map-underscore-to-camel-case: true),dept_name 就能自动装进 Emp 的 deptName 属性。(课程最终版改成了直接起 deptName 别名,效果一样。)本机实测(原始分页版工程,请求第 2 页、每页 5 条)——服务端日志里就是两条 SQL,和 PPT 的职责表完全对上:
1==> Preparing: select count(*) from emp left join dept on emp.dept_id = dept.id2==> Preparing: select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id limit ?,?3==> Parameters: 5(Integer), 5(Integer)三点值得记:① 第一条 select count(*) 就是 count() 方法,是自己写的;② 第二条末尾的 limit ?,? 是SQL 里手写的;③ Parameters: 5(Integer), 5(Integer) 里的第一个 5 是起始索引,由 Service 那行 (2 - 1) * 5 = 5 算出来——日志里看到的这个 5,就是”页码换起始索引”这一步的物证。
把代码和 66 篇的分析对照,会发现分页这件事被拆成了三段,每一段都要自己写:
| 手工点 | 写在哪 | 具体是什么 |
|---|---|---|
| ① 总数要自己查 | Mapper | 额外写一个 count() 方法 + select count(*) … 语句 |
| ② 起始索引要自己算 | Service | Integer start = (page - 1) * pageSize; |
| ③ 分页要自己拼 | Mapper | SQL 结尾手写 limit #{start},#{pageSize} |
写一次两次还行,但员工、班级、学员、日志……每个分页接口都要把这三件事重复一遍。这就是 PageHelper 要解决的问题。
PPT 第 50 页的定义:
PageHelper 是第三方提供的在 Mybatis 框架中用来实现分页的插件,用来简化分页操作,提高开发效率。
“插件”这个词意味着:它不用改 MyBatis 本身,只要把它加进依赖,它就能”挂”在 MyBatis 执行 SQL 的流程里干活。PPT 把原始方式和 PageHelper 的代码并排放在一起,对比非常直观:
| 层 | 原始方式 | PageHelper |
|---|---|---|
| Mapper | 两个方法:count() + list(start, pageSize)(SQL 里带 limit) | 一个方法:list()(SQL 里不带分页,就是一条普通的条件查询) |
| Service | count() → 算 start → list(start, pageSize) → new PageResult(total, empList) | PageHelper.startPage(page, pageSize) → list() → 解析结果 |
也就是说,分页的痕迹从 Mapper 里消失了:数据访问层只写”查员工数据”这一件事,分页参数从业务层”临时加”进去。
PPT 第 51 页把用法拆成三步。
① pom.xml 引依赖
1<!-- 分页插件PageHelper -->2<dependency>3 <groupId>com.github.pagehelper</groupId>4 <artifactId>pagehelper-spring-boot-starter</artifactId>5 <version>1.4.7</version>6</dependency>三个坐标值要记准:com.github.pagehelper / pagehelper-spring-boot-starter / 1.4.7(课程工程用的就是 1.4.7)。用 starter 而不是裸的 pagehelper,是因为它带了 Spring Boot 的自动配置——加完依赖直接就能用,不用再写配置类。
② Mapper:写一个”不带分页”的查询方法
1/**2 * 查询员工信息3 */4@Select("select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id")5public List<Emp> list();对比原始方式的 list(Integer start, Integer pageSize):参数没了,SQL 末尾的 limit 也没了,方法变成一条”查全部员工(带部门名称)“的普通查询。
③ Service:设置分页参数、调用、解析结果
1@Override2public PageResult<Emp> page(Integer page, Integer pageSize) {3 //1. 设置分页参数4 PageHelper.startPage(page, pageSize);5
6 //2. 执行查询7 List<Emp> empList = empMapper.list();8
9 //3. 解析查询结果,并封装10 Page<Emp> p = (Page<Emp>) empList;11 return new PageResult<>(p.getTotal(), p.getResult());12}第 3 步的两行是初学者最容易懵的地方,PPT 第 51 页配的那张图正好解释了原因:

Page<E> 继承 ArrayList<E>、ArrayList<E> 实现 List<E>,所以 Mapper 返回的 List<Emp> 实际上就是一个 Page<Emp>,可以强转PageHelper.startPage(...) 之后的第一条查询,MyBatis 返回的 List 并不是普通的 ArrayList,而是 PageHelper 的 Page 对象(它继承 ArrayList,所以是 List 的”子类”)——向上转型没问题,转回来(强转)也没问题;Page 上额外带了分页信息,代码里用到两个方法——p.getTotal() 拿总记录数(等价于原始方式里 count() 查出来的值),p.getResult() 拿当前页的数据列表(等价于原始方式里 list(start, pageSize) 的结果)。所以 new PageResult<>(p.getTotal(), p.getResult()) 这一行,干的就是原始方式里”总数 + 列表”两件事——只不过两个值都不用自己查了。
PPT 第 52 页把机制画成了一张流程:Mapper 里写的那条”不带分页”的 SQL,交给 PageHelper 之后变成了两句——count(0) 和带 limit 的查询,分别产出”总记录数”和”数据列表”,最后封装成 Page<Emp> 返回。

PageHelper.startPage(page, pageSize) 在调用 Mapper 之前执行,查询回来的 List 被强转成 Page<Emp> 取出 total 与当前页数据也就是说,原始方式里的”三处手工”,PageHelper 全接管了:它自动生成一条 select count(0) … 查总数、自动给原 SQL 拼上 limit 取当前页。

SELECT count(0) 拿到总数(Total: 1),第二条原 SQL 末尾被加上了 LIMIT ?, ?(参数 5, 5)拿到 5 行数据这张日志图不是示意图,本机实测跑出来的一模一样。下面把三次请求的日志并排放:
1# ① 带日期条件(/emps?begin=2007-09-01&end=2022-09-01&page=1&pageSize=5)2==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id WHERE e.entry_date BETWEEN ? AND ?3==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id4 WHERE e.entry_date between ? and ? order by e.update_time desc LIMIT ?5
6# ② 带姓名 + 性别条件(/emps?name=李&gender=1&page=1&pageSize=5)7==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id WHERE e.name LIKE concat('%', ?, '%') AND e.gender = ?8==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id9 WHERE e.name like concat('%', ?, '%') and e.gender = ? order by e.update_time desc LIMIT ?10
11# ③ 不带任何条件、翻到第 2 页(/emps?page=2&pageSize=5)12==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id13==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id14 order by e.update_time desc LIMIT ?, ?对着日志能读出四件事:
SELECT count(0) … 拿总数、一条原 SQL 加 LIMIT 拿当前页数据——Mapper 里明明只写了一条 SQL,PageHelper 把它变成了两条;count(0) 是它自动生成的(原始方式里我们自己写的是 count(*))——两者作用一样,都是数行数,看到 count(0) 不要以为写错了;count(0) 的条件和原 SQL 的条件一致:第 ① 组的 WHERE e.entry_date BETWEEN ? AND ?、第 ② 组的 WHERE e.name LIKE … AND e.gender = ?——总数必须是”符合条件的总数”,不然页码会算错;LIMIT 的参数:第 ③ 组(第 2 页、每页 5 条)的 LIMIT ?, ? 参数就是 5, 5——和原始方式里 Service 算出来的起始索引一模一样,只是这次不用自己算。本机实测的三次请求响应也对得上:无条件、每页 10 条时 data 是 {"total":30,"rows":[10 条]};带日期条件时 total 变成 20、rows 5 条;第 2 页、每页 5 条时 total 还是 30、rows 从 id=30 那条开始(列表按 update_time desc 排,最新的排最前)。total 永远是”符合条件的总数”,与当前是第几页无关——这也是分页条能正确画出来的前提。
PPT 第 53 页在日志下面单独列了两条,都是会真出错的地方:
① SQL 语句结尾不要加分号(;)
分号是”这条 SQL 说完了”的意思,而 PageHelper 要在原 SQL 后面继续拼 limit——结尾带着分号,拼出来就是 … ; LIMIT ? 这种语法不合法的 SQL,查询直接失败。原始方式里 limit 是自己写在末尾的,加不加分号”看起来没事”,所以从原始方式切到 PageHelper 时,很容易把分号一起带过来。
② PageHelper 只会对紧跟在其后的第一条 SQL 语句进行分页处理
PageHelper.startPage(page, pageSize) 并不是立刻去查数据库,它只是把”分页参数”暂存起来,等紧接着执行的第一条查询来认领——这条查询用掉参数后,暂存的分页信息就没了。所以:
1PageHelper.startPage(page, pageSize);2List<Emp> empList = empMapper.list(); // ✅ 分页参数被这条查询用掉了中间如果插了别的查询(比如先查一次部门列表、或者先调了别的方法),分页参数就会被那条”插队”的 SQL 用掉,真正想分页的查询反而变成全量查询——而且不会报错,只是数据不对,特别难查。规则很简单:startPage 后面第一行就调用要分页的那个 Mapper 方法。
| 对比项 | 原始方式 | PageHelper |
|---|---|---|
| 额外依赖 | 无 | pagehelper-spring-boot-starter(1.4.7) |
| Mapper 方法 | count() + list(start, pageSize) 两个 | list() 一个,SQL 里不带分页 |
| 总数怎么来 | 自己写 select count(*) … | 插件自动生成 select count(0) … |
| 起始索引怎么来 | Service 里 (page - 1) * pageSize | 插件根据 startPage(page, pageSize) 自己算(日志里能看到 LIMIT 5, 5) |
limit 写在哪 | Mapper 的 SQL 末尾手写 | 插件自动拼到原 SQL 后面 |
| Service 代码量 | 5 行左右(查两次 + 算 + 封装) | 3 行(startPage + 查 + 解析) |
| 最容易踩的坑 | 起始索引算错、两句 SQL 的条件不一致 | SQL 结尾加分号、startPage 后面不是目标查询 |
一句话总结:原始方式让你明白分页的每一步在做什么,PageHelper 把这些步骤交给插件做——实际项目里用 PageHelper,但要看得懂日志里那两句 SQL 是哪来的。
PageResult;Mapper 写 select count(*) … 和 select … limit … 两条 SQLcount() 方法 ② Service 里 Integer start = (page - 1) * pageSize; ③ SQL 末尾手写 limit #{start},#{pageSize}@RequestParam(defaultValue = "1") / defaultValue = "10"——不传就是第 1 页、每页 10 条com.github.pagehelper:pagehelper-spring-boot-starter:1.4.7 ② Mapper 写不带分页的查询方法 ③ Service 里 PageHelper.startPage(page, pageSize) 后调用 Mapper、再把结果解析成 PageResultselect count(0) … 查总数 + 给原 SQL 加上 limit 查当前页数据,结果封装成 Page 返回Page<Emp> p = (Page<Emp>) empList;(Page 继承 ArrayList,所以能强转),p.getTotal() 是总记录数、p.getResult() 是当前页数据startPage 只对紧跟其后的第一条 SQL 生效select count(*) … + 手写 limit ?,?(Parameters: 5(Integer), 5(Integer));PageHelper 生成的是 SELECT count(0) … + 自动拼的 LIMIT ?, ?(第 2 页、每页 5 条时参数为 5, 5) 2-1 用原始方式实现员工分页查询
需求:不引任何插件,自己实现分页。控制层对外提供查询接口,接收页码和每页记录数(不传时默认第 1 页、每页 10 条);业务层先拿到总记录数、再算出”本页从第几条开始取”、把两者封装成分页结果返回;数据访问层写两条 SQL(一条数总数、一条取当前页数据,都要带出部门名称并按最后修改时间倒序)。
(练习文件 test_67_分页查询实现.java 的题目2-1 里给了写作区。)
一级 · 思路:按三层分工写——控制层只负责收参数和返回结果;业务层负责”查两次 + 算一次 + 封装一次”;数据访问层只有两条 SQL
二级 · 方法:控制层用 @RequestParam(defaultValue = "1") / defaultValue = "10";业务层用 (page - 1) * pageSize 算起始索引、new PageResult<>(total, empList) 封装;数据访问层用 @Select 写 select count(*) … 与 select … limit #{start},#{pageSize},方法返回 List<Emp>
三级 · 骨架:public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize) + Long total = empMapper.____(); Integer start = (____) * ____; List<Emp> empList = empMapper.list(____, ____); return new PageResult<>(____, ____); + @Select("select count(*) from emp left join dept on emp.dept_id = dept.id") / @Select("select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id limit #{start},#{pageSize}")
控制层:
1@GetMapping2public Result page(@RequestParam(defaultValue = "1") Integer page,3 @RequestParam(defaultValue = "10") Integer pageSize) {4 log.info("分页请求参数: {}, {}", page, pageSize);5 PageResult<Emp> pageResult = empService.page(page, pageSize);6 return Result.success(pageResult);7}业务层:
1@Override2public PageResult<Emp> page(Integer page, Integer pageSize) {3 //1. 获取总记录数 total4 Long total = empMapper.count();5 //2. 获取分页查询结果列表 rows6 Integer start = (page - 1) * pageSize;7 List<Emp> empList = empMapper.list(start, pageSize);8 //3. 封装分页结果9 return new PageResult<>(total, empList);10}数据访问层:
1@Select("select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id limit #{start},#{pageSize}")2public List<Emp> list(Integer start, Integer pageSize);3
4@Select("select count(*) from emp left join dept on emp.dept_id = dept.id")5Long count();说明:defaultValue 让接口不传参也能跑(默认第 1 页、每页 10 条);start 一定要用 (page - 1) * pageSize 算,页码不能直接当起始索引(第 1 页的起始索引是 0,不是 1)。本机实测这次请求(第 2 页、每页 5 条)的日志是 select count(*) … 和 … limit ?,?,参数是 5(Integer), 5(Integer)——第一个 5 就是 (2-1)*5 算出来的。
2-2 用分页插件把上面的实现改一遍
需求:不改接口文档、不改返回结构,只把”自己写数总数的 SQL、自己算起始索引、自己写分页关键字”这三件事交给分页插件:工程里加上插件依赖;数据访问层只留一条不带分页的查询;业务层开启分页、执行查询、再从结果里取出总记录数和当前页数据封装返回。
(练习文件 test_67_分页查询实现.java 的题目2-2 里给了写作区。)
一级 · 思路:删掉 count() 方法和 SQL 里的 limit,在业务层”查询之前”开启分页,查询结果要”转一下”才能拿到总数
二级 · 方法:依赖坐标 com.github.pagehelper:pagehelper-spring-boot-starter:1.4.7;业务层调 PageHelper.startPage(page, pageSize);结果用 Page<Emp> p = (Page<Emp>) empList; 强转后取 p.getTotal() 和 p.getResult()
三级 · 骨架:<!-- 分页插件PageHelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> + @Select("select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id") public List<Emp> ____(); + PageHelper.____(page, pageSize); List<Emp> empList = empMapper.____(); Page<Emp> p = (____) empList; return new PageResult<>(____, ____);
pom.xml:
1<!-- 分页插件PageHelper -->2<dependency>3 <groupId>com.github.pagehelper</groupId>4 <artifactId>pagehelper-spring-boot-starter</artifactId>5 <version>1.4.7</version>6</dependency>数据访问层(只留一条不带分页的查询):
1@Select("select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id")2public List<Emp> list();业务层:
1@Override2public PageResult<Emp> page(Integer page, Integer pageSize) {3 //1. 设置分页参数4 PageHelper.startPage(page, pageSize);5 //2. 执行查询6 List<Emp> empList = empMapper.list();7 //3. 解析查询结果,并封装8 Page<Emp> p = (Page<Emp>) empList;9 return new PageResult<>(p.getTotal(), p.getResult());10}说明:控制层一个字都不用改(接口对外还是收 page/pageSize、还是返回 PageResult),这就是”插件式改造”的好处。两处细节:① Page 继承 ArrayList,所以 List<Emp> 能强转成 Page<Emp>,转完才有 getTotal()/getResult();② startPage 后面第一行就必须是 empMapper.list(),中间不能插别的查询。本机实测:改造后服务端日志里出现了自动生成的 SELECT count(0) … 和自动拼上的 LIMIT ?, ?——Mapper 里的 SQL 一个字没改,分页却生效了。
2-3 说清两种方式差在哪、各有什么坑
需求:不看代码,用自己的话回答三个问题:① 从原始方式换成 PageHelper,哪三处从”手工”变成了”自动”?② 插件自动生成的总数 SQL 长什么样、和我们自己写的那条有什么区别?③ 用分页插件时最容易踩的两个坑是什么、为什么会踩?
(练习文件 test_67_分页查询实现.java 的题目2-3 里给了答题区。)
一级 · 思路:三处手工对着 66 篇的两句 SQL + 换算公式数;两个坑一个是”SQL 结尾”,一个是”谁用掉了分页参数”
二级 · 方法:自动的三处 = 查总数(count(0))、算起始索引(LIMIT 的参数)、拼 limit;坑一 = SQL 结尾的分号;坑二 = startPage 后面紧跟着的查询不是目标查询
三级 · 骨架:① count() 方法 → ____ 自动查;起始索引 → 插件自己算;SQL 末尾的 limit → 插件自动拼。② 我们写 count(*),插件生成 ____。③ 分号会被拼进 … ; LIMIT ?;分页参数被 ____ 用掉。
① 三处变化:原本要自己写 count() 方法和 select count(*) …(现在插件自动补一条 SELECT count(0) …);原本要自己在 Service 里算 (page - 1) * pageSize(现在插件根据 startPage 的参数自己算,日志里 LIMIT 的参数就是它);原本要在 SQL 末尾手写 limit #{start},#{pageSize}(现在插件自动拼到原 SQL 后面)。Mapper 那边则从两个方法变成一个”不带分页”的方法。
② 两条总数 SQL:我们写的是 select count(*) from emp left join dept on emp.dept_id = dept.id;插件生成的是 SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id——count(*) 和 count(0) 作用一样(都是数符合条件的行数),条件部分也一致,所以总数不会错。
③ 两个坑:一是 SQL 结尾加了分号——PageHelper 要在原 SQL 后面拼 limit,拼成 … ; LIMIT ? 就语法错误了(原始方式里 limit 本来就在末尾,所以很容易忘了删分号);二是 startPage 后面紧跟的不是目标查询——分页参数会被”插队”的那条 SQL 用掉,真正要分页的查询变成全量查询,而且不报错、只是数据不对。所以规矩是:startPage 之后第一行就调用要分页的 Mapper 方法,并且 SQL 结尾不要分号。
2-4 排错:分页没生效 / 报错了,先查哪里
需求:三个同学用分页插件之后都出了问题,请分别说出原因和改法:
① 小 A 的数据访问层 SQL 结尾习惯性多写了一个符号,一请求就报 SQL 语法错误;
② 小 B 在开启分页之后先调了一次”查部门列表”的方法,再去查员工——结果员工列表返回了全部 30 条,总数也是 30;
③ 小 C 的业务层把”当前页数据的条数”当成了总记录数返回(他直接取了查询结果列表的长度)——分页能翻,但页码条上的总数一直是 5(每页 5 条)。
(练习文件 test_67_分页查询实现.java 的题目2-4 里给了答题区。)
一级 · 思路:三个问题分别对应 PPT 第 53 页的两条注意事项 + Page 对象的意义
二级 · 方法:① 分号与拼接;② startPage 只对紧跟其后的第一条 SQL 生效;③ Page 上带的 getTotal() 才是总记录数,List.size() 只是当前页条数
三级 · 骨架:① 删掉 SQL 结尾的 ____;② 把 startPage 移到 ____ 之前(或把要分页的查询紧跟在 startPage 后面);③ 把 empList.size() 换成 ((Page<Emp>) empList).____()
① 原因:PageHelper 要在原 SQL 后面拼上 limit,末尾的分号会让拼出来的 SQL 变成 … ; LIMIT ?,语法不合法。改法:把 SQL 结尾的分号删掉(原始方式里 limit 是自己写在末尾的,从原始方式切过来时最容易带这个分号)。
② 原因:startPage 只是把分页参数暂存起来,只对紧跟其后的第一条 SQL 生效——“查部门列表”那条 SQL 把参数用掉了,轮到查员工时已经没有分页参数,于是变成全量查询(total 自然也是全部 30 条)。改法:PageHelper.startPage(page, pageSize); 之后第一行就调用 empMapper.list()(要分页的那条查询),中间不要插别的查询。
③ 原因:List.size() 拿到的是当前页的条数(每页 5 条就是 5),不是总记录数;总记录数藏在 PageHelper 返回的 Page 对象里。改法:
1Page<Emp> p = (Page<Emp>) empList;2return new PageResult<>(p.getTotal(), p.getResult());这也解释了为什么必须先强转成 Page<Emp>:getTotal() 这个方法是 Page 上才有的(本机实测里 total 为 30,而不是每页的 5 条)。
3-1 把原始方式改成 PageHelper,并用两次的 SQL 日志验证 这一题把 66 篇的骨架和本节的两种实现连起来做一遍:先跑通原始方式、再改成 PageHelper,用日志证明”三处手工”确实被接管了。
tlias-web-management 里已有 emp、dept 表(本机 emp 30 条、dept 6 条),application.yml 开着 MyBatis 的 SQL 日志(log-impl: org.apache.ibatis.logging.stdout.StdOutImpl);page/pageSize(默认 1 和 10);GET /emps?page=2&pageSize=5,把控制台里这两条日志抄下来,并回答:Parameters 里的两个 5 分别是什么?GET /emps?page=2&pageSize=5,把这次的两条日志抄下来,并回答:这次的两条 SQL 分别是谁生成的?LIMIT 的参数是几?GET /emps?page=1&pageSize=10,回答:total 是几?它和 pageSize、页码有关系吗?涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 三层架构 | 第 2、4 步——两种方式都只改数据访问层和业务层,控制层不动 |
| limit 与起始索引 | 第 2、3 步——原始方式自己算 (page-1)*pageSize |
| MyBatis SQL 日志 | 第 3、5 步——用日志对比两种方式实际发出的 SQL |
| PageHelper 三步用法 | 第 4、5 步——依赖、Mapper、Service 各改一处 |
| PageHelper 的两条注意事项 | 第 5、7 步——startPage 的位置与 SQL 结尾的分号 |
一级 · 思路:先跑”自己算”的版本拿到基线日志,再换成插件版对照——重点看”总数 SQL 从哪来”和”limit 从哪来”
二级 · 方法:原始方式用 count() + limit #{start},#{pageSize};插件方式用 PageHelper.startPage(page, pageSize) + 无分页查询 + Page<Emp> 强转;日志对比看 count(*) 对 count(0)、手写 limit ?,? 对自动 LIMIT ?, ?
三级 · 骨架:第 3 步日志应形如 Preparing: select count(*) … / Preparing: select emp.* … limit ?,? / Parameters: 5(Integer), 5(Integer);第 5 步日志应形如 Preparing: SELECT count(0) … / Preparing: select e.* … LIMIT ?, ?
3. 原始方式的日志(本机实测):
1==> Preparing: select count(*) from emp left join dept on emp.dept_id = dept.id2==> Preparing: select emp.*, dept.name as dept_name from emp left join dept on emp.dept_id = dept.id limit ?,?3==> Parameters: 5(Integer), 5(Integer)Parameters 里的两个 5:第一个是起始索引((2 - 1) × 5 = 5,由业务层算出来),第二个是每页记录数(pageSize = 5)。
5. PageHelper 的日志(本机实测):
1==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id2==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id order by e.update_time desc LIMIT ?, ?两条都是插件生成的:SELECT count(0) … 是它自动补的总数查询(Mapper 里根本没有这个方法),LIMIT ?, ? 是它自动拼到原 SQL 末尾的;LIMIT 的参数是 5, 5(起始索引、每页记录数),和原始方式算出来的一样,只不过这次不用自己算。
6. total:GET /emps?page=1&pageSize=10 的 data 是 {"total":30,"rows":[10 条]}——total 是 30(emp 表的全部记录数),它只和查询条件有关,和页码、每页条数无关;第 2 页的 total 同样是 30。前端正是拿 total 和 pageSize 算出”共几页”(30 ÷ 10 = 3 页)来画分页条。
7. 加分号:SQL 结尾写分号,PageHelper 拼 limit 时会拼成 … ; LIMIT ?,SQL 语法不合法、请求报错。原因是分号表示”这条 SQL 已经结束了”,而插件的工作方式就是在 SQL 后面继续加东西——所以用 PageHelper 时 SQL 结尾一律不加分号(原始方式里 limit 是自己写在末尾的,切过来时最容易忘)。
如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



"所有的相遇都是久别重逢。"—— 未知


