Spring + MyBatis/MyBatis-Plus 分页方案(limit分页和游标分页)详解
版本说明
Spring Boot : 3.1.xMyBatis : 3.5.xMyBatis-Plus : 3.5.xPageHelper : 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;
}