1. 项目概述与核心价值
这个实战项目将带你完整掌握MyBatis-Plus和PageHelper在企业级开发中的核心用法。不同于官方文档的碎片化示例,我会通过一个可立即运行的Spring Boot项目,演示如何将这两个强力工具组合使用,解决实际业务中最头疼的分页查询问题。
你可能已经遇到过这些场景:前端需要带条件的分页列表,后端既要处理复杂查询又要保证性能;不同数据库(MySQL/Oracle)的分页语法差异导致兼容性问题;分页查询需要同时返回总记录数和其他聚合数据。这些正是本项目的靶心所在。
关键提示:本文配套项目已托管在GitHub,包含完整的Controller-Service-Mapper三层架构实现,克隆后只需配置数据库即可运行测试所有示例。
2. 环境准备与项目初始化
2.1 技术栈选型理由
- Spring Boot 2.7.x:当前企业主流稳定版本,避免最新版可能的兼容性问题
- MyBatis-Plus 3.5.x:对MyBatis的增强工具,提供通用CRUD操作
- PageHelper 5.3.x:最流行的MyBatis物理分页插件
- H2 Database:内存数据库,方便测试无需额外安装
- Lombok:减少样板代码,聚焦核心逻辑
xml复制<!-- 关键依赖示例 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>5.3.2</version>
</dependency>
2.2 项目结构设计
code复制src/main/java
├── com.example.demo
│ ├── config # 存放MyBatis配置类
│ ├── controller # 分页API入口
│ ├── entity # 数据库实体类
│ ├── mapper # MyBatis-Plus Mapper接口
│ ├── service # 业务逻辑层
│ └── vo # 视图对象
3. MyBatis-Plus核心功能实战
3.1 实体与Mapper快速开发
使用MyBatis-Plus的@TableName和@TableField注解定义实体:
java复制@Data
@TableName("sys_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("username")
private String name;
private Integer age;
private String email;
}
Mapper接口只需继承BaseMapper即可获得全套CRUD方法:
java复制public interface UserMapper extends BaseMapper<User> {
// 自定义方法写在后面
}
3.2 条件构造器高级用法
QueryWrapper是MyBatis-Plus的查询神器:
java复制// 复杂查询示例
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "username as name", "age")
.like("username", "张")
.between("age", 20, 30)
.orderByDesc("create_time");
踩坑提醒:字段别名在PageHelper分页时会失效,建议在VO中做转换
3.3 自定义SQL与分页冲突解决
当需要手写复杂SQL时,注意与PageHelper的兼容性:
xml复制<!-- XML中编写自定义SQL -->
<select id="selectUserWithRole" resultType="com.example.demo.vo.UserVO">
SELECT u.*, r.role_name
FROM sys_user u LEFT JOIN sys_role r ON u.role_id = r.id
WHERE ${ew.customSqlSegment}
</select>
Java调用时:
java复制// 保证PageHelper在Wrapper前调用
PageHelper.startPage(1, 10);
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("u.deleted", 0);
userMapper.selectUserWithRole(wrapper);
4. PageHelper深度集成指南
4.1 基础分页实现
最简单的分页方式:
java复制@GetMapping("/users")
public PageInfo<User> listUsers(@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userService.list();
return new PageInfo<>(users);
}
4.2 分页参数传递最佳实践
推荐使用专用DTO接收前端参数:
java复制@Data
public class PageParam {
@Min(1)
private Integer pageNum = 1;
@Min(1) @Max(100)
private Integer pageSize = 10;
private String orderBy;
private Boolean count = true;
}
4.3 多数据库兼容方案
在application.yml中配置方言:
yaml复制pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
针对Oracle的特殊处理:
java复制if (isOracle()) {
PageHelper.startPage(pageNum, pageSize).setOrderBy("ROWNUM ASC");
}
5. 企业级分页功能进阶
5.1 分页+多表联查优化
使用JOIN+子查询解决N+1问题:
java复制@Select("SELECT u.* FROM (SELECT id FROM sys_user WHERE #{ew.sqlSegment} LIMIT #{pageSize}) t " +
"JOIN sys_user u ON t.id = u.id")
List<User> pageJoin(@Param("ew") Wrapper wrapper,
@Param("pageSize") Long pageSize);
5.2 分页数据脱敏处理
结合Jackson注解实现:
java复制@Data
public class UserVO {
private Long id;
private String name;
@JsonIgnore
private String password;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String email;
}
5.3 百万级数据分页方案
采用"游标分页"替代传统分页:
java复制public List<User> scrollPage(Long lastId, int size) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt(lastId != null, "id", lastId)
.orderByAsc("id")
.last("LIMIT " + size);
return userMapper.selectList(wrapper);
}
6. 项目中的典型问题排查
6.1 PageHelper不生效的常见原因
- 调用顺序错误:必须在Mapper方法前调用
startPage() - 线程污染:异步场景未清理PageLocal
- 拦截器冲突:检查MyBatis配置的拦截器顺序
6.2 分页总数不准的解决方案
java复制// 手动执行count查询
Page<?> page = PageHelper.startPage(1, 10).setCount(true);
List<User> list = userMapper.selectList(null);
long total = page.getTotal();
6.3 MyBatis-Plus与PageHelper版本冲突
推荐版本组合:
- MyBatis-Plus ≥ 3.4.0
- PageHelper ≥ 5.2.0
冲突表现:分页后结果集仍返回全部数据
7. 项目扩展与生产建议
7.1 统一分页响应封装
java复制@Data
public class PageResult<T> {
private Integer pageNum;
private Integer pageSize;
private Long total;
private List<T> list;
public static <T> PageResult<T> of(PageInfo<T> pageInfo) {
PageResult<T> result = new PageResult<>();
result.setPageNum(pageInfo.getPageNum());
// 其他字段赋值...
return result;
}
}
7.2 性能监控与调优
添加Logback配置监控SQL执行时间:
xml复制<logger name="com.github.pagehelper" level="DEBUG"/>
<logger name="com.baomidou.mybatisplus" level="INFO"/>
7.3 安全防护建议
- 限制最大pageSize(建议≤100)
- 对orderBy参数进行白名单校验
- 敏感字段必须手动排除
8. 项目运行与测试指南
8.1 启动准备
- 克隆项目:
git clone https://github.com/xxx/mybatis-plus-pagehelper-demo.git - 导入IDE(推荐IntelliJ IDEA)
- 启动
DemoApplication
8.2 接口测试示例
使用Postman测试分页接口:
code复制GET /users?pageNum=2&pageSize=5&orderBy=age desc
响应示例:
json复制{
"pageNum": 2,
"pageSize": 5,
"total": 87,
"list": [
{
"id": 6,
"name": "张三",
"age": 28
}
]
}
8.3 单元测试要点
java复制@Test
public void testPageQuery() {
// 必须放在Mapper方法前执行
PageHelper.startPage(1, 5);
List<User> users = userMapper.selectList(null);
assertEquals(5, users.size());
assertTrue(users instanceof Page);
}
在实际开发中,我发现合理使用MyBatis-Plus的Lambda表达式能让代码更健壮:
java复制// 推荐使用LambdaQueryWrapper
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.select(User::getId, User::getName)
.ge(User::getAge, 18)
.orderByAsc(User::getCreateTime);
这种写法在重构时特别有用——字段名修改后IDE会直接提示编译错误,而字符串形式的风险则要大得多。
