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;
}
}
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> {
}
三、游标分页实现(手动分页)
1. 请求参数类
public class CursorParam {
private Integer pageSize = 10;
private Long lastCursor;
}
2. 响应参数类
public class CursorResult<T> {
private List<T> list;
private Boolean hasNext;
private Long nextCursor;
}
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) {
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;
}