Spring + MyBatis/MyBatis-Plus 分页方案(limit分页和游标分页)详解
版本说明
- Spring Boot: 3.1.x
- MyBatis: 3.5.x
- MyBatis-Plus: 3.5.x
- PageHelper: 6.0.x
一、分页方式概述
1. 传统分页(LIMIT/OFFSET)
- 核心原理 :通过
LIMIT 和 OFFSET 截取数据片段
- 适用场景:后台管理系统、需跳页查询的场景
- 实现方案 :
- 手动分页(原生 SQL)
- PageHelper 插件(推荐)
- MyBatis-Plus 分页插件(推荐)
2. 游标分页(Cursor-based)
- 核心原理:基于排序字段游标(如 ID、时间戳)逐页查询
- 适用场景:移动端无限滚动、实时数据流
- 实现方案 :
- 手动分页(主流方案)
- 成熟插件:目前无广泛采用的插件,需手动实现
二、传统分页实现(LIMIT/OFFSET)
方案 1:手动分页(原生 SQL)
请求参数类
public class PageParam {
private Integer pageNum = 1; // 当前页码
private Integer pageSize = 10; // 每页数量
public Integer getOffset() {
return (pageNum - 1) * pageSize;
}
// Getter & Setter
}
Mapper 接口
@Mapper
public interface UserMapper {
List<User> selectByPage(@Param("offset") Integer offset,
@Param("pageSize") Integer pageSize);
Long selectTotalCount();
}
XML 映射文件
<!-- 分页查询 -->
<select id="selectByPage" resultType="User">
SELECT * FROM user
ORDER BY id DESC
LIMIT #{offset}, #{pageSize}
</select>
<!-- 总记录数 -->
<select id="selectTotalCount" resultType="java.lang.Long">
SELECT COUNT(*) FROM user
</select>
Service 层
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageResult<User> getUsers(PageParam param) {
List<User> users = userMapper.selectByPage(param.getOffset(), param.getPageSize());
Long total = userMapper.selectTotalCount();
return PageResult.<User>builder()
.list(users)
.total(total)
.pageNum(param.getPageNum())
.pageSize(param.getPageSize())
.build();
}
}
方案 2:PageHelper 插件(推荐)
配置插件
@Configuration
public class PageHelperConfig {
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor pageInterceptor = new PageInterceptor();
Properties props = new Properties();
props.setProperty("helperDialect", "mysql");
pageInterceptor.setProperties(props);
return pageInterceptor;
}
}
Service 层使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageInfo<User> getUsersByPage(PageParam param) {
PageHelper.startPage(param.getPageNum(), param.getPageSize());
List<User> users = userMapper.selectAll(); // 无需手动拼接分页参数
return new PageInfo<>(users);
}
}
Mapper 接口
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user")
List<User> selectAll();
}
方案 3:MyBatis-Plus 分页插件(推荐)
配置插件
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
Service 层使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public Page<User> getUsersByPage(PageParam param) {
Page<User> page = new Page<>(param.getPageNum(), param.getPageSize());
return userMapper.selectPage(page, null);
}
}
Mapper 接口
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 自动获得分页能力
}
三、游标分页实现(手动分页)
1. 请求参数类
public class CursorParam {
private Integer pageSize = 10; // 每页数量
private Long lastCursor; // 上一页最后记录的游标值
// Getter & Setter
}
2. 响应参数类
public class CursorResult<T> {
private List<T> list; // 当前页数据
private Boolean hasNext; // 是否有下一页
private Long nextCursor; // 下一页起始游标
// Getter & Setter
}
3. Mapper 接口
@Mapper
public interface UserMapper {
List<User> selectByCursor(@Param("cursor") Long cursor,
@Param("pageSize") Integer pageSize);
}
4. XML 映射文件
<select id="selectByCursor" resultType="User">
SELECT * FROM user
<where>
<if test="cursor != null">
id < #{cursor}
</if>
</where>
ORDER BY id DESC
LIMIT #{pageSize}
</select>
5. Service 层逻辑
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public CursorResult<User> getUsersByCursor(CursorParam param) {
// 实际查询 pageSize + 1 条用于判断是否有下一页
List<User> users = userMapper.selectByCursor(param.getLastCursor(), param.getPageSize() + 1);
CursorResult<User> result = new CursorResult<>();
boolean hasNext = users.size() > param.getPageSize();
if (hasNext) {
result.setList(users.subList(0, param.getPageSize()));
result.setNextCursor(users.get(param.getPageSize() - 1).getId());
} else {
result.setList(users);
result.setNextCursor(null);
}
result.setHasNext(hasNext);
return result;
}
}
四、分页插件对比
| 插件/方案 |
优点 |
缺点 |
适用场景 |
| 手动分页 |
完全控制 SQL |
代码冗余,维护成本高 |
简单项目、特殊分页需求 |
| PageHelper |
零侵入,简单易用 |
依赖特定语法(PageHelper.startPage()) |
传统 MyBatis 项目 |
| MyBatis-Plus |
深度整合,支持 Lambda 表达式 |
需继承 BaseMapper |
MyBatis-Plus 项目 |
| 游标分页 |
高性能,无 OFFSET |
无法跳页 |
移动端列表、实时数据流 |
五、注意事项
- 索引优化 :确保排序字段(如
id)有索引
- 安全限制 :限制最大
pageSize(建议 ≤ 100)
- 数据一致性:分页期间数据变化可能导致结果差异
- 参数校验 :校验
pageNum ≥1,pageSize ≥1
六、扩展建议
- 统一分页响应格式:
public class R<T> {
private Integer code;
private String msg;
private T data;
private PageInfo page; // 分页元数据
}
- 动态排序支持:
<select id="selectByPage" resultType="User">
SELECT * FROM user
ORDER BY ${orderBy} ${orderDir}
LIMIT #{offset}, #{pageSize}
</select>
- Redis 缓存优化:
// 缓存总记录数
public Long getTotalCount() {
String cacheKey = "user:total";
Long total = redisTemplate.opsForValue().get(cacheKey);
if (total == null) {
total = userMapper.selectTotalCount();
redisTemplate.opsForValue().set(cacheKey, total, 5, TimeUnit.MINUTES);
}
return total;
}