# MyBatis教程 - 11 分页

当数据库表中的数据很多的时候,查询数据的时候就需要分页。

我们一般在 SQL 中使用 LIMIT 关键字,来限制查询的结果,从而实现分页的功能:

例如:

-- 返回前5条记录  
SELECT * FROM tb_user LIMIT 5;

-- 从第6条记录开始,返回5条记录(即第2页,假设每页5条)  
SELECT * FROM tb_user LIMIT 5 OFFSET 5;

-- MySQL 特有的语法,等同于上面的查询  
SELECT * FROM tb_user LIMIT 5, 5;
1
2
3
4
5
6
7
8

只需要将参数传递给 LIMIT ? OFFSET ? 就可以实现简单的分页功能了。


但是在实际的开发中,在页面需要封装分页的信息,通常是上面显示一个数据的表格,下面会显示如下的分页信息:

而页面上需要根据分页的信息显示有多少页、当前是第几页、每一页的条数、当前页的条数、总共有多少条、当前是否是第一页(用来判断是否可以点击上一页按钮)、当前是否是最后一页(用来判断是否可以点击下一页按钮)等等信息。

如果我们自己来封装这些信息,会有点麻烦,这个时候我们可以借助第三方的插件来完成。在 MyBatis 中用的比较多的就是 PageHelper (官网:https://pagehelper.github.io/)。

下面来介绍如何使用 PageHelper 插件。

# 11.2 集成PageHelper

首先在项目中集成 PageHelper 。

# 1 添加PageHelper依赖

首先在项目的 pom.xml 中集成 PageHelper 依赖:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>6.1.0</version>
</dependency>
1
2
3
4
5

添加完成,要 右键 -> Maven -> Reload project

# 2 配置PageHelper插件

在 MyBatis 的全局配置文件中,配置PageHelper插件的拦截器。

mybatis-config.xml 中配置:

<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
    </plugin>
</plugins>
1
2
3
4
5

注意 mybatis-config.xml 中配置项的顺序,<plugins> 要配置在 <environments> 前面。

# 11.2 使用PageHelper

下面看一下如何使用 PageHelper。

# 1 准备数据

首先生成一些数据,用于分页获取:

INSERT INTO `tb_user`   
(`username`, `password`, `email`, `age`, `create_time`, `update_time`)  
VALUES   
('user1', 'pwd12345', 'user1@foooor.com', 25, NOW(), NOW()),  
('user2', 'pwd67890', 'user2@foooor.com', 30, NOW(), NOW()),  
('user3', 'pwd112233', 'user3@foooor.com', 28, NOW(), NOW()),  
('user4', 'pwd445566', 'user4@foooor.com', 35, NOW(), NOW()),  
('user5', 'pwd778899', 'user5@foooor.com', 22, NOW(), NOW()),  
('user6', 'pwd001122', 'user6@foooor.com', 29, NOW(), NOW()),  
('user7', 'pwd334455', 'user7@foooor.com', 34, NOW(), NOW()),  
('user8', 'pwd667788', 'user8@foooor.com', 27, NOW(), NOW()),  
('user9', 'pwd990011', 'user9@foooor.com', 32, NOW(), NOW()),  
('user10', 'pwd223344', 'user10@foooor.com', 24, NOW(), NOW()),  
('user11', 'pwd556677', 'user11@foooor.com', 26, NOW(), NOW()),  
('user12', 'pwd889900', 'user12@foooor.com', 31, NOW(), NOW()),  
('user13', 'pwd11223344', 'user13@foooor.com', 23, NOW(), NOW()),  
('user14', 'pwd55667788', 'user14@foooor.com', 36, NOW(), NOW()),  
('user15', 'pwd99001122', 'user15@foooor.com', 21, NOW(), NOW()),  
('user16', 'pwd33445566', 'user16@foooor.com', 30, NOW(), NOW()),  
('user17', 'pwd77889900', 'user17@foooor.com', 25, NOW(), NOW()),  
('user18', 'pwd11223355', 'user18@foooor.com', 29, NOW(), NOW()),  
('user19', 'pwd55667799', 'user19@foooor.com', 34, NOW(), NOW()),  
('user20', 'pwd99001133', 'user20@foooor.com', 28, NOW(), NOW()),  
('user21', 'pwd22334466', 'user21@foooor.com', 32, NOW(), NOW()),  
('user22', 'pwd66778811', 'user22@foooor.com', 26, NOW(), NOW()),  
('user23', 'pwd00112244', 'user23@foooor.com', 31, NOW(), NOW()),  
('user24', 'pwd44556677', 'user24@foooor.com', 24, NOW(), NOW()),  
('user25', 'pwd88990011', 'user25@foooor.com', 27, NOW(), NOW()),  
('user26', 'pwd33445588', 'user26@foooor.com', 35, NOW(), NOW()),  
('user27', 'pwd77889922', 'user27@foooor.com', 23, NOW(), NOW()),  
('user28', 'pwd11223366', 'user28@foooor.com', 30, NOW(), NOW()),  
('user29', 'pwd55667700', 'user29@foooor.com', 25, NOW(), NOW()),  
('user30', 'pwd99001144', 'user30@foooor.com', 29, NOW(), NOW()),  
('user31', 'pwd22334477', 'user31@foooor.com', 33, NOW(), NOW()),  
('user32', 'pwd66778833', 'user32@foooor.com', 26, NOW(), NOW()),  
('user33', 'pwd00112255', 'user33@foooor.com', 31, NOW(), NOW()),  
('user34', 'pwd44556688', 'user34@foooor.com', 28, NOW(), NOW()),  
('user35', 'pwd88990022', 'user35@foooor.com', 32, NOW(), NOW()),  
('user36', 'pwd33445599', 'user36@foooor.com', 24, NOW(), NOW()),  
('user37', 'pwd77889933', 'user37@foooor.com', 27, NOW(), NOW()),  
('user38', 'pwd11223377', 'user38@foooor.com', 35, NOW(), NOW()),  
('user39', 'pwd55667711', 'user39@foooor.com', 23, NOW(), NOW()),  
('user40', 'pwd99001155', 'user40@foooor.com', 26, NOW(), NOW()),  
('user41', 'pwd22334488', 'user41@foooor.com', 30, NOW(), NOW()),  
('user42', 'pwd66778844', 'user42@foooor.com', 25, NOW(), NOW()),  
('user43', 'pwd00112266', 'user43@foooor.com', 29, NOW(), NOW()),  
('user44', 'pwd44556699', 'user44@foooor.com', 34, NOW(), NOW()),  
('user45', 'pwd88990033', 'user45@foooor.com', 28, NOW(), NOW());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 2 编写接口方法

编写接口方法和 Mapper.xml 中的 SQL 映射。

UserMapper.java

/**
 * 查询所有用户
 */
List<User> selectAll();
1
2
3
4

UserMapper.xml

<!-- 定义查询所有用户的 SQL 语句 -->
<select id="selectAll" resultType="User">
    SELECT * FROM tb_user
</select>
1
2
3
4

定义的是查询所有的数据,不需要针对分页做额外的处理,待会 PageHelper 会处理。

# 3 使用PageHelper分页

使用 PageHelper 分页非常的简单,直接编写测试代码:

@Test
public void testSelectByPager() {
    // 获取SqlSession连接
    SqlSession sqlSession = MyBatisUtils.getSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 开启分页查询,第一个参数为当前页,第二个参数为每页的记录数
    PageHelper.startPage(1, 5);
    List<User> userList = userMapper.selectAll();

    // 获取分页信息
    PageInfo pageInfo = new PageInfo(userList);

    log.info("userList size: {}", userList.size());
    log.info("pageInfo: {}", pageInfo);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面在查询方法之前调用 PageHelper.startPage 方法,紧跟在这个方法后的第一个 MyBatis 查询方法 会被进行分页。

所以虽然我们没有对 userMapper.selectAll() 进行分页处理,PageHelper 也会对查询进行拦截,并添加 limit 命令。

执行结果如下,可以看到会有两条执行 SQL,一条是查询总条数,一条是进行分页查询:


PageInfo 包含了非常全面的分页属性,包含了 Page 对象信息,完整的 PageInfo 信息如下:

PageInfo{
    pageNum=1, 
    pageSize=5, 
    size=5, 
    startRow=1, 
    endRow=5, 
    total=45, 
    pages=9, 
    list=Page{
        count=true, 
        pageNum=1, 
        pageSize=5, 
        startRow=0, 
        endRow=5, 
        total=45, 
        pages=9, 
        reasonable=false, 
        pageSizeZero=false}[User(id=1, username=user1, password=pwd12345, email=user1@foooor.com, age=25, createTime=Tue Aug 27 18:20:26 CST 2024, updateTime=Tue Aug 27 18:20:26 CST 2024), User(id=2, username=user2, password=pwd67890, email=user2@foooor.com, age=30, createTime=Tue Aug 27 18:20:26 CST 2024, updateTime=Tue Aug 27 18:20:26 CST 2024), User(id=3, username=user3, password=pwd112233, email=user3@foooor.com, age=28, createTime=Tue Aug 27 18:20:26 CST 2024, updateTime=Tue Aug 27 18:20:26 CST 2024), User(id=4, username=user4, password=pwd445566, email=user4@foooor.com, age=35, createTime=Tue Aug 27 18:20:26 CST 2024, updateTime=Tue Aug 27 18:20:26 CST 2024), User(id=5, username=user5, password=pwd778899, email=user5@foooor.com, age=22, createTime=Tue Aug 27 18:20:26 CST 2024, updateTime=Tue Aug 27 18:20:26 CST 2024)], 
    prePage=0, 
    nextPage=2, 
    isFirstPage=true, 
    isLastPage=false, 
    hasPreviousPage=false, 
    hasNextPage=true, 
    navigatePages=8, 
    navigateFirstPage=1, 
    navigateLastPage=8, 
    navigatepageNums=[1, 2, 3, 4, 5, 6, 7, 8]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

PageInfo 包含了分页的所有重要信息,以下是 PageInfo 参数的解释:

  • pageNum=1:当前页码,这里是第1页。
  • pageSize=5:每页显示的记录数,这里是5条。
  • size=5:当前页实际显示的记录数,这里是5条,与pageSize相同,表示没有数据被省略。
  • startRow=1:当前页第一条记录在总结果集中的位置,这里是第1条。
  • endRow=5:当前页最后一条记录在总结果集中的位置,这里是第5条。
  • total=45:总记录数,这里是45条。
  • pages=9:总页数,这里是9页。
  • list:当前页的数据列表,包含5个User对象。
  • prePage=0:前一页的页码,这里是0,表示没有前一页。
  • nextPage=2:后一页的页码,这里是2,表示下一页是第2页。
  • isFirstPage=true:是否是第一页,这里是true
  • isLastPage=false:是否是最后一页,这里是false
  • hasPreviousPage=false:是否有前一页,这里是false
  • hasNextPage=true:是否有下一页,这里是true
  • navigatePages=8:导航分页的页码数,这里是8,表示分页导航条显示的页码数量。
  • navigateFirstPage=1:导航分页条的第一页页码,这里是1。
  • navigateLastPage=8:导航分页条的最后一页页码,这里是8。
  • navigatepageNums=[1, 2, 3, 4, 5, 6, 7, 8]:导航分页条中显示的页码列表。

PageInfo 提供了丰富的分页信息,可以很方便地实现分页功能,并且在前端展示分页导航。

# 11.3 分页的原理

当调用 PageHelper.startPage 方法时,它实际上是将分页参数(页码、每页条数等)存储到了一个与当前线程绑定的 ThreadLocal 变量中。ThreadLocal 保证了每个线程的分页参数是隔离的,互不干扰。

PageHelper 是基于 MyBatis 的拦截器(Interceptor)机制,当 MyBatis 执行 SQL 查询时, PageHelper 的拦截器会检测到这个查询。并检查 ThreadLocal 中是否有分页参数。如果检测到分页参数,拦截器会根据这些参数动态地修改原始的 SQL 语句,通常是添加 LIMIT 子句来限制查询结果的数量,以及可能需要一个子查询或计算来确定总记录数。

# 11.4 使用注意点

下面这样的代码,可能会导致出问题:

PageHelper.startPage(1, 10);
List<User> list;
if(param1 != null){
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<User>();
}
1
2
3
4
5
6
7

上面这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

上面这个代码,应该写成下面这个样子:

List<User> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<User>();
}
1
2
3
4
5
6
7

这种写法就能保证安全。