Skip to content

Mybatis 分页

逻辑分页(内存操作)

逻辑分页是查询全部数据,然后在内存中进行分页处理。

优点:

  • 实现简单
  • 数据库无关

缺点:

  • 性能差,内存占用大
  • 不适合大数据量
  • 网络传输量大

实现原理

java
// 逻辑分页是指从数据库查询出所有数据,然后在应用程序内存中对结果集进行分页处理。
// 通过select * from table_name 查询所有数据,用list装入

    /**
     * 手动逻辑分页 直接通过后台代码操作查询的所有数据,相当于内存操作
     */
public PageResult<User> getUsersByLogicalPage(int pageNum, int pageSize) {
    // 1. 查询所有数据
    List<User> allUsers = userMapper.selectAllUsers();

    // 2. 计算分页参数
    int total = allUsers.size();
    int startIndex = (pageNum - 1) * pageSize;
    int endIndex = Math.min(startIndex + pageSize, total);

    // 3. 截取当前页数据
    List<User> pageData = new ArrayList<>();
    if (startIndex < total) {
        pageData = allUsers.subList(startIndex, endIndex);
    }

    // 4. 封装分页结果
    PageResult<User> result = new PageResult<>();
    result.setData(pageData);
    result.setTotal(total);
    result.setPageNum(pageNum);
    result.setPageSize(pageSize);
    result.setPages((total + pageSize - 1) / pageSize);
    result.setHasNext(pageNum < result.getPages());
    result.setHasPrev(pageNum > 1);

    return result;
}

物理分页(io/硬盘操作)

物理分页直接在数据库层面进行分页,只查询需要的数据。

优点:

  • 性能好,内存占用少
  • 适合大数据量场景
  • 减少网络传输

缺点:

  • 需要编写不同数据库的分页SQL
  • 实现相对复杂

实现原理

sql
SELECT * FROM user 
ORDER BY id 
LIMIT #{offset}, #{pageSize}

PageHelper分页插件

1. 依赖pom.xml

xml
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version> <!-- 或其他适用于你 Spring Boot 的版本 -->
</dependency>

2. 配置分页插件

application.yml 中添加配置:

yml
pagehelper:
  helper-dialect: mysql         # 数据库方言(mysql, oracle, postgresql 等)
  reasonable: true              # 分页合理化(超出末页时返回末页)
  support-methods-arguments: true # 支持接口参数
  params: count=countSql        # 统计查询配置
  page-size-zero: true          # 允许 pageSize=0 返回全部结果

3. 使用分页插件

在 Service 层使用分页:

java
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;

    public PageInfo<User> findUsers(int pageNum, int pageSize) {
        // 关键:在查询前设置分页参数
        PageHelper.startPage(pageNum, pageSize);
        
        // 紧跟着的第一个查询会被分页
        List<User> users = userMapper.selectAllUsers();
        
        // 用 PageInfo 包装结果(包含分页信息)
        return new PageInfo<>(users);
    }
}

4. Controller 示例

java
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public PageInfo<User> getUsers(
            @RequestParam(defaultValue = "1") int page, 
            @RequestParam(defaultValue = "10") int size) {
        return userService.findUsers(page, size);
    }
}

5. 分页结果示例

java
// PageInfo 详解
public class PageInfo<T> {
    private int pageNum;        // 当前页
    private int pageSize;       // 每页的数量
    private int size;           // 当前页的数量
    private String orderBy;     // 排序
    private int startRow;       // 当前页面第一个元素在数据库中的行号
    private int endRow;         // 当前页面最后一个元素在数据库中的行号
    private long total;         // 总记录数
    private int pages;          // 总页数
    private List<T> list;       // 结果集
    private int prePage;        // 前一页
    private int nextPage;       // 下一页
    private boolean isFirstPage;// 是否为第一页
    private boolean isLastPage; // 是否为最后一页
    private boolean hasPreviousPage; // 是否有前一页
    private boolean hasNextPage;     // 是否有下一页
    private int navigatePages;       // 导航页码数
    private int[] navigatepageNums;  // 所有导航页号
    private int navigateFirstPage;   // 导航条上的第一页
    private int navigateLastPage;    // 导航条上的最后一页
}

6.注意

sql
# 不能加;结束符
select *
from user_info
where user_id like CONCAT('%', #{userId}, '%')
  and nick_name like CONCAT('%', #{nickName}, '%')

分页性能优化

1、索引优化

sql
-- 为排序字段添加索引
CREATE INDEX idx_user_create_time ON user(create_time);

-- 复合索引优化
CREATE INDEX idx_user_status_time ON user(status, create_time);

2、 避免深分页问题

sql
-- 问题:  当offset很大时,性能急剧下降
SELECT * FROM user ORDER BY id LIMIT 1000000, 10;
sql
-- 使用游标分页
-- 记录上次查询的最后一个ID
SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 10;
sql
-- 延迟关联
-- 先查询ID,再关联查询详细信息
SELECT u.* FROM user u 
INNER JOIN (
    SELECT id FROM user ORDER BY id LIMIT 1000000, 10
) t ON u.id = t.id;

3、count 查询优化

sql
-- 问题:
-- 直接使用 PageHelper 插件代理查询,由于直接在原始复杂的业务sql最外层套上 count(*),
-- 由于业务本身的sql复杂这样相当于这样复杂的sql查询了两次。前者的count(*)比后者还要耗时大些。
select count(*) from (`原始业务sql`)

-- count(*)在大表上很慢
SELECT COUNT(*) FROM user;
java
// 不直接使用 PageHelper 查询的计数,根据自己的业务逻辑自己自定义一个count sql
PageHelper.startPage(pageNum, pageSize,false); // false 关闭计数 ,默认是true

// 自定义计算count
Integer count = userGroupInfoMapper.selectCountByGroupIdAndType(groupNumber, associateType, goodsType);
// 封装数据
PageInfo<OpUserGroupInfoDomain> pageInfo = new PageInfo<>(userInfoDTOList);
// 设置 total
pageInfo.setTotal(count);

PageHelper 分页的 count 原理

核心原理:动态方法查找 + AOP 拦截

sql
# count 计数 pageHelper会内部构造一个 查询方法名_COUNT的 mapper,然后在原始查询sql上面套上一层
select count(*) from (`原始sql`) 
# 这个如果开发者自己写了一个后缀为_COUNT 的 mappe接口 那么PageHelper插件会直接用个ampper进行计算count。

1、PageHelper 的拦截机制

java
// PageHelper 是基于 MyBatis 的 Interceptor(拦截器) 实现的:
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
// 拦截所有查询操作
}

2、执行流程

java
// 1. PageHelper.startPage() 设置分页参数到 ThreadLocal 开启分页
PageHelper.startPage(1, 10);

// 2. 执行查询时被拦截器捕获
List<User> list = mapper.getOpUserGroupinfo(params);

3、拦截器内部处理

java
public Object intercept(Invocation invocation) {
    // 获取原始的 MappedStatement
    MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    String originalId = ms.getId(); // 例如:com.demo.UserMapper.getOpUserGroupinfo
    
    // 1. 构造 count 方法 ID
    String countId = originalId + "_COUNT"; // com.demo.UserMapper.getOpUserGroupinfo_COUNT
    
    // 2. 查找是否存在自定义 count 方法
    Configuration configuration = ms.getConfiguration();
    MappedStatement countMs = null;
    
    try {
        countMs = configuration.getMappedStatement(countId);
        // 找到了!使用自定义 count
    } catch (Exception e) {
        // 没找到,使用默认 count 生成逻辑
        countMs = generateDefaultCount(ms);
    }
    
    // 3. 执行 count 查询
    long total = executeCount(countMs, invocation.getArgs()[1]);
    
    // 4. 执行原始分页查询
    return executePageQuery(invocation, total);
}

如有转载或 CV 的请标注本站原文地址

访客数 --| 总访问量 --