MyBatis-Plus 分页插件与员工信息分页查询
纲要
本篇引入 MyBatis-Plus 分页插件,实现带条件过滤的员工列表查询。
- 分页插件配置 :
MybatisPlusInterceptor+PaginationInnerInterceptor - 接口契约 :
GET /employee/page?page=1&pageSize=10&name=xxx - 返回值类型 :
R<Page>------ 由前端需要的records与total决定 - 三步实现:构造分页构造器 → 构造条件构造器 → 执行查询
- 条件构造器 :
like模糊查询 +StringUtils.isNotEmpty动态拼接 +orderByDesc排序 - 前端状态渲染 :
ElementUI作用域插槽把status的0/1转成「已禁用 / 正常」
功能需求
X外卖菜单
- 订单明细
- 分类管理
- 菜品管理
- 套餐管理
- 员工管理(当前选中)
员工管理
修改密码\] \[退出登录
请输入员工姓名\] \[搜索\] \[添加员工
| 员工姓名 | 账号 | 手机号 | 操作 |
|---|---|---|---|
| 管理员 | 13333333333 | 13333333333 | 编辑 禁用 |
| 管理员 | 13333333333 | 13333333333 | 编辑 禁用 |
| 管理员 | 13333333333 | 13333333333 | 编辑 禁用 |
| 管理员 | 13333333333 | 13333333333 | 编辑 禁用 |
| 管理员 | 13333333333 | 13333333333 | 编辑 禁用 |
| ... | ... | ... | ... |
分页:总共 85 个项目 < 1 2 3 4 5 >
交互说明
-
搜索
- 1.1 输入员工姓名进行搜索,支持模糊搜索
-
添加员工
- 2.1 点击添加员工跳转至添加员工页面
- 2.2 字段:
- 2.2.1 账号:必填,限制输入3-20个字符,限制输入字母及阿拉伯数字,验证唯一性
- 2.2.2 默认密码:123456,系统生成,仅展示,不可编辑
- 2.2.3 姓名:必填,限制输入1-12个字,验证唯一性
- 2.2.4 性别:默认男性,可选择
- 2.2.5 身份证号:验证身份证规则
-
用户列表
- 3.1 显示字段:账号、姓名、手机号、操作
- 3.2 排序:创建时间倒序排列,单页最多显示10条
-
操作
- 4.1 编辑:可以修改员工信息
- 4.2 禁用/启用:用户被禁用后,不可登录
需求分析
员工数据量大时,一次性全量展示既慢又乱。标准做法是分页展示:
#mermaid-svg-2OAdmJIxhUeuELIU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2OAdmJIxhUeuELIU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2OAdmJIxhUeuELIU .error-icon{fill:#552222;}#mermaid-svg-2OAdmJIxhUeuELIU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2OAdmJIxhUeuELIU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU .marker.cross{stroke:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2OAdmJIxhUeuELIU p{margin:0;}#mermaid-svg-2OAdmJIxhUeuELIU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster-label text{fill:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster-label span{color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster-label span p{background-color:transparent;}#mermaid-svg-2OAdmJIxhUeuELIU .label text,#mermaid-svg-2OAdmJIxhUeuELIU span{fill:#333;color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .node rect,#mermaid-svg-2OAdmJIxhUeuELIU .node circle,#mermaid-svg-2OAdmJIxhUeuELIU .node ellipse,#mermaid-svg-2OAdmJIxhUeuELIU .node polygon,#mermaid-svg-2OAdmJIxhUeuELIU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .rough-node .label text,#mermaid-svg-2OAdmJIxhUeuELIU .node .label text,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape .label,#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape .label{text-anchor:middle;}#mermaid-svg-2OAdmJIxhUeuELIU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .rough-node .label,#mermaid-svg-2OAdmJIxhUeuELIU .node .label,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape .label,#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape .label{text-align:center;}#mermaid-svg-2OAdmJIxhUeuELIU .node.clickable{cursor:pointer;}#mermaid-svg-2OAdmJIxhUeuELIU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU .arrowheadPath{fill:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2OAdmJIxhUeuELIU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2OAdmJIxhUeuELIU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2OAdmJIxhUeuELIU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2OAdmJIxhUeuELIU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2OAdmJIxhUeuELIU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2OAdmJIxhUeuELIU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster text{fill:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster span{color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2OAdmJIxhUeuELIU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2OAdmJIxhUeuELIU rect.text{fill:none;stroke-width:0;}#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape p,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape .label rect,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2OAdmJIxhUeuELIU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2OAdmJIxhUeuELIU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2OAdmJIxhUeuELIU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工具条
分页条 el-pagination
每页条数下拉:10 / 20 / 30 / 40
页码:1 2 3 ...
跳页输入框
数据表格 el-table
账号 / 姓名 / 手机号 / 性别 / 身份证号 / 账号状态 / 操作
姓名输入框 + 查询按钮
GET /employee/page?name=xxx
三个交互点都需要后端支持:
| 交互 | 传递的参数变化 |
|---|---|
| 首次进入页面 | page=1&pageSize=10 |
| 切换每页条数 | page=1&pageSize=20(重置回第一页) |
| 点击页码 / 输入姓名查询 | page=2&pageSize=10&name=张三 |
关键点 :分页与过滤是叠加关系。输入姓名后如果结果超过一页,必须同时按姓名过滤并分页,不能只做其一。
分页插件配置
MyBatis-Plus 的分页能力不是默认开启的,必须注册拦截器。
src/main/java/com/itheima/reggie/config/MybatisPlusConfig.java:
java
package com.itheima.reggie.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
工作原理
MySQL PaginationInnerInterceptor MybatisPlusInterceptor IService.page Controller MySQL PaginationInnerInterceptor MybatisPlusInterceptor IService.page Controller #mermaid-svg-in7qj4BbCs1gfbzA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-in7qj4BbCs1gfbzA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-in7qj4BbCs1gfbzA .error-icon{fill:#552222;}#mermaid-svg-in7qj4BbCs1gfbzA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-in7qj4BbCs1gfbzA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-in7qj4BbCs1gfbzA .marker.cross{stroke:#333333;}#mermaid-svg-in7qj4BbCs1gfbzA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-in7qj4BbCs1gfbzA p{margin:0;}#mermaid-svg-in7qj4BbCs1gfbzA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-in7qj4BbCs1gfbzA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-in7qj4BbCs1gfbzA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-in7qj4BbCs1gfbzA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA .sequenceNumber{fill:white;}#mermaid-svg-in7qj4BbCs1gfbzA #sequencenumber{fill:#333;}#mermaid-svg-in7qj4BbCs1gfbzA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA .messageText{fill:#333;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-in7qj4BbCs1gfbzA .labelText,#mermaid-svg-in7qj4BbCs1gfbzA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .loopText,#mermaid-svg-in7qj4BbCs1gfbzA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-in7qj4BbCs1gfbzA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-in7qj4BbCs1gfbzA .noteText,#mermaid-svg-in7qj4BbCs1gfbzA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-in7qj4BbCs1gfbzA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-in7qj4BbCs1gfbzA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-in7qj4BbCs1gfbzA .actorPopupMenu{position:absolute;}#mermaid-svg-in7qj4BbCs1gfbzA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-in7qj4BbCs1gfbzA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-in7qj4BbCs1gfbzA .actor-man circle,#mermaid-svg-in7qj4BbCs1gfbzA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-in7qj4BbCs1gfbzA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page(pageInfo, queryWrapper) 执行查询,被拦截器拦截 交给分页拦截器 SELECT COUNT(*) FROM employee WHERE ... total = 25 SELECT * FROM employee WHERE ... LIMIT 0,10 当前页 10 条 把结果塞回 pageInfo 对象 返回 pageInfo(已含 records 与 total)
插件做了两件事:
- 自动执行一条
COUNT(*)查询,把总数写入Page.total - 自动在业务
SQL后追加LIMIT offset, size,把当前页数据写入Page.records
版本差异提示
MyBatis-Plus 3.4.0 前后 API 有变化:
| 版本 | 写法 |
|---|---|
3.4.0 之前 |
new PaginationInterceptor() |
3.4.0 及之后 |
new MybatisPlusInterceptor() + addInnerInterceptor(new PaginationInnerInterceptor()) |
本项目用 3.4.2,必须用新写法。用旧类会编译失败。
接口契约分析
请求
按 F12 观察首次进入员工管理页的请求:
text
GET http://localhost:8080/employee/page?page=1&pageSize=10
在姓名框输入「张三」点查询:
text
GET http://localhost:8080/employee/page?page=1&pageSize=10&name=张三
三个参数都是普通 key=value 的查询串,不是 JSON ,因此不能用 @RequestBody,直接声明方法参数即可。
返回类型为什么是 Page
前端处理响应的代码:
javascript
getMemberList().then(res => {
if (String(res.code) === '1') {
this.tableData = res.data.records || []
this.counts = res.data.total
}
})
前端从 res.data 中读取 records 与 total 两个属性。这决定了后端 data 必须是一个含有这两个属性的对象 ------ 而 MyBatis-Plus 的 Page 类恰好具备:
Page 属性 |
含义 |
|---|---|
records |
当前页数据列表 |
total |
总记录数 |
size |
每页条数 |
current |
当前页码 |
pages |
总页数 |
所以方法签名是 R<Page> 而不是 R<Employee> 或 R<List<Employee>>。返回类型由前端需要的数据结构倒推得出,这是前后端分离开发的基本方法。
Controller 实现
三步实现
#mermaid-svg-IOBp8m8qij42OvEp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IOBp8m8qij42OvEp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IOBp8m8qij42OvEp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IOBp8m8qij42OvEp .error-icon{fill:#552222;}#mermaid-svg-IOBp8m8qij42OvEp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IOBp8m8qij42OvEp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IOBp8m8qij42OvEp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IOBp8m8qij42OvEp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IOBp8m8qij42OvEp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IOBp8m8qij42OvEp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IOBp8m8qij42OvEp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IOBp8m8qij42OvEp .marker.cross{stroke:#333333;}#mermaid-svg-IOBp8m8qij42OvEp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IOBp8m8qij42OvEp p{margin:0;}#mermaid-svg-IOBp8m8qij42OvEp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster-label text{fill:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster-label span{color:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster-label span p{background-color:transparent;}#mermaid-svg-IOBp8m8qij42OvEp .label text,#mermaid-svg-IOBp8m8qij42OvEp span{fill:#333;color:#333;}#mermaid-svg-IOBp8m8qij42OvEp .node rect,#mermaid-svg-IOBp8m8qij42OvEp .node circle,#mermaid-svg-IOBp8m8qij42OvEp .node ellipse,#mermaid-svg-IOBp8m8qij42OvEp .node polygon,#mermaid-svg-IOBp8m8qij42OvEp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .rough-node .label text,#mermaid-svg-IOBp8m8qij42OvEp .node .label text,#mermaid-svg-IOBp8m8qij42OvEp .image-shape .label,#mermaid-svg-IOBp8m8qij42OvEp .icon-shape .label{text-anchor:middle;}#mermaid-svg-IOBp8m8qij42OvEp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .rough-node .label,#mermaid-svg-IOBp8m8qij42OvEp .node .label,#mermaid-svg-IOBp8m8qij42OvEp .image-shape .label,#mermaid-svg-IOBp8m8qij42OvEp .icon-shape .label{text-align:center;}#mermaid-svg-IOBp8m8qij42OvEp .node.clickable{cursor:pointer;}#mermaid-svg-IOBp8m8qij42OvEp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IOBp8m8qij42OvEp .arrowheadPath{fill:#333333;}#mermaid-svg-IOBp8m8qij42OvEp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IOBp8m8qij42OvEp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IOBp8m8qij42OvEp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IOBp8m8qij42OvEp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IOBp8m8qij42OvEp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IOBp8m8qij42OvEp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IOBp8m8qij42OvEp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .cluster text{fill:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster span{color:#333;}#mermaid-svg-IOBp8m8qij42OvEp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IOBp8m8qij42OvEp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IOBp8m8qij42OvEp rect.text{fill:none;stroke-width:0;}#mermaid-svg-IOBp8m8qij42OvEp .icon-shape,#mermaid-svg-IOBp8m8qij42OvEp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IOBp8m8qij42OvEp .icon-shape p,#mermaid-svg-IOBp8m8qij42OvEp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IOBp8m8qij42OvEp .icon-shape .label rect,#mermaid-svg-IOBp8m8qij42OvEp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IOBp8m8qij42OvEp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IOBp8m8qij42OvEp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IOBp8m8qij42OvEp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page / pageSize / name 入参
① 构造分页构造器
new Page(page, pageSize)
② 构造条件构造器
LambdaQueryWrapper
③ 执行查询
employeeService.page(pageInfo, queryWrapper)
返回 R.success(pageInfo)
完整代码
java
/**
* 员工信息分页查询
* @param page 当前页码
* @param pageSize 每页条数
* @param name 员工姓名(可选过滤条件)
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
log.info("page = {},pageSize = {},name = {}", page, pageSize, name);
// 构造分页构造器
Page pageInfo = new Page(page, pageSize);
// 构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
// 添加过滤条件:name 非空时才拼接 LIKE
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
// 添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 执行查询
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
逐行解析
参数接收 :int page, int pageSize, String name
Spring MVC 会按参数名从请求参数中取值。这依赖于编译时保留参数名(-parameters),Spring Boot 默认已开启。若参数名对不上,会报 500 而非静默置 null ------ 这类错误容易定位。
new Page(page, pageSize) :第一个参数是当前页(从 1 开始),第二个是每页条数。Page 是 MyBatis-Plus 的分页对象,泛型可省略(省略时是原始类型)。
动态条件 like(condition, column, value):
java
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
like 的第一个参数 boolean condition 是条件开关 :为 true 时才把这个 LIKE 拼进 SQL。等价于:
java
// 等价的手工写法,更啰嗦
if (StringUtils.isNotEmpty(name)) {
queryWrapper.like(Employee::getName, name);
}
这是 MyBatis-Plus 的条件构造器惯例:所有方法都提供带 condition 首参的重载,用于动态 SQL。
为什么用 like 而不是 eq?姓名查询通常是模糊匹配,用户输入「张」希望查出「张三」「张四」。like 生成的 SQL 是 WHERE name LIKE '%张%'。
StringUtils.isNotEmpty :来自 org.apache.commons.lang.StringUtils(commons-lang 2.6)。注意与 org.springframework.util.StringUtils.hasLength 的区别 ------ 两者都能判空,但导入错了会编译失败。
orderByDesc(Employee::getUpdateTime) :按更新时间倒序,最近修改的排最前。生成的 SQL 是 ORDER BY update_time DESC。
为什么不需要接收返回值:
java
employeeService.page(pageInfo, queryWrapper); // 没有赋值给新变量
page 方法内部会把查询结果回填到传入的 pageInfo 对象 中。所以直接返回 pageInfo 即可。这是 MyBatis-Plus 的一个约定,容易误以为要写 Page result = service.page(...)。
生成的 SQL
无姓名条件时:
sql
SELECT COUNT(*) FROM employee;
SELECT id, name, username, phone, sex, id_number, status, create_time, update_time, create_user, update_user
FROM employee
ORDER BY update_time DESC
LIMIT 0,10;
有姓名条件时:
sql
SELECT COUNT(*) FROM employee WHERE (name LIKE '%张三%');
SELECT id, name, username, phone, sex, id_number, status, create_time, update_time, create_user, update_user
FROM employee
WHERE (name LIKE '%张三%')
ORDER BY update_time DESC
LIMIT 0,10;
前端状态列的渲染处理
现象
后端返回的 status 是数字 1,页面上却显示「正常」。这不是后端做的转换,而是前端处理的。
ElementUI 作用域插槽
html
<el-table-column label="账号状态">
<template slot-scope="scope">
{{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
</template>
</el-table-column>
slot-scope="scope" 是 ElementUI 的作用域插槽,用于获取当前行数据:
| 表达式 | 含义 |
|---|---|
scope |
插槽作用域对象 |
scope.row |
当前行的数据对象 |
scope.row.status |
当前行的 status 字段值 |
三元表达式把 0 映射成「已禁用」,其他值映射成「正常」。
对比:不做处理会怎样
如果改成普通的 prop 绑定:
html
<el-table-column prop="status" label="账号状态"></el-table-column>
页面上会直接显示原始数字 1。用户看到 1 无法理解,所以前端做这层映射是必要的。
这种「后端存状态码、前端做展示映射」是常见模式。另一种做法是在后端用 @JsonFormat 或自定义序列化器转换,但会增加后端与前端展示的耦合,通常不推荐。
完整可运行代码
目录结构
text
src/main/java/com/itheima/reggie/
├── ReggieApplication.java
├── common/
│ ├── R.java
│ ├── BaseContext.java
│ ├── GlobalExceptionHandler.java
│ └── JacksonObjectMapper.java
├── config/
│ ├── MybatisPlusConfig.java # 本篇新增:分页插件
│ └── WebMvcConfig.java
├── controller/
│ └── EmployeeController.java # login / logout / save / page
├── entity/
│ └── Employee.java
├── filter/
│ └── LoginCheckFilter.java
├── mapper/
│ └── EmployeeMapper.java
└── service/
├── EmployeeService.java
└── impl/
└── EmployeeServiceImpl.java
MybatisPlusConfig.java
java
package com.itheima.reggie.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
EmployeeController.java(分页部分)
java
package com.itheima.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
/**
* 员工信息分页查询
* @param page 当前页码
* @param pageSize 每页条数
* @param name 员工姓名,用于模糊查询,可为空
* @return 分页对象,含 records(当前页数据)与 total(总记录数)
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
log.info("page = {},pageSize = {},name = {}", page, pageSize, name);
// 构造分页构造器
Page pageInfo = new Page(page, pageSize);
// 构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
// 添加过滤条件:仅当 name 非空时拼接 LIKE
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
// 添加排序条件:按更新时间倒序
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 执行查询,结果回填到 pageInfo
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
}
Employee.java
java
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 员工实体
*/
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;//身份证号码
private Integer status;
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private Long updateUser;
}
前端 member.js 接口封装
javascript
// src/main/resources/backend/api/member.js
function getMemberList(params) {
return $axios({
url: '/employee/page',
method: 'get',
params
})
}
前端分页与查询逻辑
javascript
// src/main/resources/backend/page/member/list.html
new Vue({
data() {
return {
input: '', // 姓名输入框
counts: 0, // 总记录数
page: 1, // 当前页
pageSize: 10, // 每页条数
tableData: [] // 表格数据
}
},
created() {
this.init()
},
methods: {
async init() {
const params = {
page: this.page,
pageSize: this.pageSize,
name: this.input ? this.input : undefined
}
await getMemberList(params).then(res => {
if (String(res.code) === '1') {
this.tableData = res.data.records || []
this.counts = res.data.total
}
})
},
// 每页条数变化
handleSizeChange(val) {
this.pageSize = val
this.init()
},
// 页码变化
handleCurrentChange(val) {
this.page = val
this.init()
},
// 点击查询按钮
handleQuery() {
this.page = 1
this.init()
}
}
})
功能测试
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 首次进入 | 登录后进员工管理页 | 显示第 1 页,每页 10 条,总条数正确 |
| 切换每页条数 | 下拉改选 20 |
重新查询,LIMIT 0,20 |
| 翻页 | 点击第 2 页 |
page=2,LIMIT 10,10 |
| 姓名查询 | 输入「张」点查询 | LIKE '%张%',结果被过滤 |
| 空姓名 | 清空输入框点查询 | LIKE 不拼接,返回全部 |
控制台日志验证
text
==> Preparing: SELECT COUNT(*) FROM employee
==> Parameters:
<== Columns: COUNT(*)
<== Row: 3
==> Preparing: SELECT id,name,username,phone,sex,id_number,status,create_time,update_time,create_user,update_user FROM employee ORDER BY update_time DESC LIMIT ?
==> Parameters: 10(Long)
<== Total: 3
带姓名时:
text
==> Preparing: SELECT COUNT(*) FROM employee WHERE (name LIKE ?)
==> Parameters: %张%(String)
注意 COUNT(*) 与数据查询是两条独立的 SQL ------ 这是分页插件的工作方式,也是分页查询在大表上性能敏感的原因(COUNT(*) 需要扫全表)。
API 速览
API / 注解 / 类 |
所属 | 作用 |
|---|---|---|
@Configuration |
Spring |
声明配置类 |
@Bean |
Spring |
把方法返回值注册为容器 Bean |
MybatisPlusInterceptor |
MyBatis-Plus 3.4+ |
MP 插件总拦截器 |
PaginationInnerInterceptor |
MyBatis-Plus 3.4+ |
分页插件,自动 COUNT 与 LIMIT |
Page<T> |
MyBatis-Plus |
分页对象,含 records / total / size / current |
IService.page(Page, Wrapper) |
MyBatis-Plus |
分页查询,结果回填到传入的 Page |
LambdaQueryWrapper<T> |
MyBatis-Plus |
Lambda 条件构造器 |
like(boolean, SFunction, Object) |
MyBatis-Plus |
条件为 true 时拼接 LIKE |
orderByDesc(SFunction) |
MyBatis-Plus |
降序排序 |
@GetMapping |
Spring MVC |
映射 GET 请求 |
StringUtils.isNotEmpty(String) |
commons-lang |
判断字符串非 null 且非空串 |
log.info("{} {} {}", a, b, c) |
SLF4J |
多占位符日志,按顺序对应 |
slot-scope |
ElementUI |
作用域插槽,获取当前行数据 |
官方文档
总结
这篇博客围绕「员工管理」的分页查询,把 MyBatis-Plus 分页插件的用法完整走了一遍。核心就三步:先注册 MybatisPlusInterceptor 拦截器,再在 Controller 里构造 Page 和 LambdaQueryWrapper,最后调用 employeeService.page() 把结果回填进 Page 返回给前端。
几个容易踩的坑也顺带提了:3.4.0 前后分页插件类名不一样、like 的条件开关怎么用、page() 不需要接收返回值、前端 status 数字要靠作用域插槽转成文字。把这些点记住,分页查询基本就不会卡壳了。
如果还想深入,建议去翻一下官方文档里条件构造器的其他方法(eq、ge、between 等),组合起来能覆盖绝大多数查询场景。