8.1 概述

给数据查询结果分页的作用无需多说。使用SELECT查询时,如果结果集数据量很大,从数据库中一次性全部取出来,不管是在内存中处理还是在页面中显示,都是既没有必要也会极大地影响软件的性能。因此要实现分页的功能,每次选择结果集中的一部分(一页)处理和显示。

不同数据库对查询结果分页的支持方法不同。类似MySQL、DB2这样的数据库,SQL 查询有专用的字句“物理”地支持分页,类似 Oracle 数据库则通过“伪列”或分析函数来实现分页的功能。不管具体使用的什么方式,实现数据分页时都有比较多的开发工作需要做。

为了增强代码的适应性(尽可能与实际的数据库系统类型无关),同时也为了简化代码编写的复杂性,我们需要选择合适的开发库(或插件)处理数据库查询结果分页。本章我们讨论如何应用Page Pelper实现分页的功能。

8.2 项目依赖与参数配置

8.1.1 项目依赖

首先我们去MVN Repository查询Page Helper的最新版本,然后在pom.xml中配置依赖(然后重新加载)

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

8.1.2 MyBatis插件配置

Page Helper有很多的配置方法,我们选择在mybatis-config.xml中配置插件的方法

    <plugins>
        <!--注意这里要写成PageInterceptor, 5.0之前的版本都是写PageHelper, 5.0之后要换成PageInterceptor-->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="helperDialect" value="mariadb" />
            <property name="reasonable" value="true" />
        </plugin>
    </plugins>

8.2 数据分页应用开发实例

作为通用的插件,Page Helper开发者设计了多种应用开发的模式(详见本章末的连接)。在这些模式中中,作者推荐使用PageHelper.startPage(或PageHelper.offsetPage )方法进行Mapper接口方式的调用。

使用这种方法,你不必对实体类、映射接口和映射定义做任何修改,需要做的仅仅是在要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法,紧跟在这个方法后的第一个MyBatis 查询方法会根据调用PageHelper.startPage的参数自动分页。

8.2.1 直接指定分页参数

在当前的单元测试类UserMapperTest中增加如下的方法:

    @Test
    @Rollback
    @Transactional
    public void testPage() {
        //在原来一条数据的基础上再增加6条数据
        userMapper.insert(new UserEntry("aa2", "13701111234"));
        userMapper.insert(new UserEntry("bb3", "18601234567"));
        userMapper.insert(new UserEntry("cc4", "18801885678"));
        userMapper.insert(new UserEntry("dd5", "18601234567"));
        userMapper.insert(new UserEntry("ee6", "18801885678"));
        userMapper.insert(new UserEntry("ff7", "18801885678"));

        //获取第1页,3条内容,默认查询总数count
        PageHelper.startPage(1, 3);
        //紧跟着的第一个select方法会被分页
        List<UserEntry> list = userMapper.getAll();

        //这里我们可以看到实际返回的结果list类型是Page<E>
        System.out.println("The resule is:");
        System.out.println(list.toString());

        //如果想取出分页信息,需要强制转换为Page<E> 或者 用PageInfo包装结果
        //PageInfo中关于分页的信息非常丰富,
        PageInfo<UserEntry> pageInfo = new PageInfo<>(list);
        System.out.println("\nThe pageInfo is:");
        System.out.println("PageNum = " + pageInfo.getPageNum());
        System.out.println("PageSize = " + pageInfo.getPageSize());
        System.out.println("StartRow = " + pageInfo.getStartRow());
        System.out.println("EndRow = " + pageInfo.getEndRow());
        System.out.println("Total = " + pageInfo.getTotal());
        System.out.println("Pages = " + pageInfo.getPages());
        System.out.println("IsFirstPage = " + pageInfo.isIsFirstPage());
        System.out.println("IsLastPage = " + pageInfo.isIsLastPage());
        System.out.println("HasPreviousPage = " + pageInfo.isHasPreviousPage());
        System.out.println("HasNextPage = " + pageInfo.isHasNextPage());

        //遍历当前页的全部数据
        System.out.println("\nThe userList is:");
        for (UserEntry userEntry : list) {
            System.out.println(userEntry);
        }

        //PageHelper.startPage的作用是一次性的,
        //后面的查询不会被分页,除非再次调用PageHelper.startPage
        list = userMapper.getAll();
        System.out.println("\nThe size is:" + list.size());
    }

单独执行这个测试方法,下面是我们得到的输出

2021-11-02 14:50:34.566  INFO 51313 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@3f57bcad testClass = UserMapperTest, testInstance = com.longser.union.cloud.data.mapper.UserMapperTest@9281d19, testMethod = testPage@UserMapperTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1e8b7643 testClass = UserMapperTest, locations = '{}', classes = '{class com.longser.union.cloud.CloudApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3c3d9b6b, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1e66f1f5, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@757277dc, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@37883b97, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@a1cdc6d, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@16d04d3d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]; transaction manager [org.springframework.jdbc.support.JdbcTransactionManager@36ad5f2a]; rollback [true]
The resule is:
Page{count=true, pageNum=1, pageSize=3, startRow=0, endRow=3, total=7, pages=3, reasonable=true, pageSizeZero=false}[UserEntry(id=1, userName=David, nickName=Grace Runner, mobile=18801681588, password=, gender=MALE, degree=BACHELOR), UserEntry(id=300, userName=aa2, nickName=null, mobile=13701111234, password=, gender=MALE, degree=NONE), UserEntry(id=301, userName=bb3, nickName=null, mobile=18601234567, password=, gender=MALE, degree=NONE)]

The pageInfo is:
PageNum = 1
PageSize = 3
StartRow = 1
EndRow = 3
Total = 7
Pages = 3
IsFirstPage = true
IsLastPage = false
HasPreviousPage = false
HasNextPage = true

The userList is:
UserEntry(id=1, userName=David, nickName=Grace Runner, mobile=18801681588, password=, gender=MALE, degree=BACHELOR)
UserEntry(id=300, userName=aa2, nickName=null, mobile=13701111234, password=, gender=MALE, degree=NONE)
UserEntry(id=301, userName=bb3, nickName=null, mobile=18601234567, password=, gender=MALE, degree=NONE)

The size is:7
2021-11-02 14:50:36.182  INFO 51313 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@3f57bcad testClass = UserMapperTest, testInstance = com.longser.union.cloud.data.mapper.UserMapperTest@9281d19, testMethod = testPage@UserMapperTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1e8b7643 testClass = UserMapperTest, locations = '{}', classes = '{class com.longser.union.cloud.CloudApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3c3d9b6b, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1e66f1f5, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@757277dc, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@37883b97, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@a1cdc6d, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@16d04d3d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]

接下来把分页的参数改成获取第2页

        PageHelper.startPage(2, 3);

再次执行后,从输出内容中可以体会到分页状态的变化

The resule is:
Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=7, pages=3, reasonable=true, pageSizeZero=false}[UserEntry(id=308, userName=cc4, nickName=null, mobile=18801885678, password=, gender=MALE, degree=NONE), UserEntry(id=309, userName=dd5, nickName=null, mobile=18601234567, password=, gender=MALE, degree=NONE), UserEntry(id=310, userName=ee6, nickName=null, mobile=18801885678, password=, gender=MALE, degree=NONE)]

The pageInfo is:
PageNum = 2
PageSize = 3
StartRow = 4
EndRow = 6
Total = 7
Pages = 3
IsFirstPage = false
IsLastPage = false
HasPreviousPage = true
HasNextPage = true

The userList is:
UserEntry(id=308, userName=cc4, nickName=null, mobile=18801885678, password=, gender=MALE, degree=NONE)
UserEntry(id=309, userName=dd5, nickName=null, mobile=18601234567, password=, gender=MALE, degree=NONE)
UserEntry(id=310, userName=ee6, nickName=null, mobile=18801885678, password=, gender=MALE, degree=NONE)

The size is:7

现在取第3页(本例的最后一页)

        PageHelper.startPage(3, 3);

再次比较一下结果信息中的状态变化

The resule is:
Page{count=true, pageNum=3, pageSize=3, startRow=6, endRow=9, total=7, pages=3, reasonable=true, pageSizeZero=false}[UserEntry(id=317, userName=ff7, nickName=null, mobile=18801885678, password=, gender=MALE, degree=NONE)]

The pageInfo is:
PageNum = 3
PageSize = 3
StartRow = 7
EndRow = 7
Total = 7
Pages = 3
IsFirstPage = false
IsLastPage = true
HasPreviousPage = true
HasNextPage = false

The userList is:
UserEntry(id=317, userName=ff7, nickName=null, mobile=18801885678, password=, gender=MALE, degree=NONE)

The size is:7

把上面的测试代码复制到UserMapperXmlTest,把userMapper改成userMapperXml,分页同样可以正常的工作。

好了,使用Page Helper在MyBatis中分页其实就是这么简单!

8.2.2 安全调用

使用PageHelper.startPage有个非常严格的条件,就是他的下一条执行的程序语句应该是要做分页的数据查询。如果你写出下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<UserEntry> list;
if(param1 != null){
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<UserEntry>();
}

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

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

List<UserEntry> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<UserEntry>();
}

这种写法就能保证代码安全。

8.3 重要提示

在使用Page Helper的时候,我们需要知道如下的要点:

  • 不要在系统中配置多个分页插件。
  • 只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会被分页。
  • 分页插件不支持带有for update语句的分页,会抛出运行时异常,对于这样的SQL建议手动分页,毕竟这样的SQL需要重视。
  • 分页插件不支持嵌套结果映射。由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确

    8.4 插件参数说明

    Page Helper提供了多个可选参数,这里选择部分说明如下(更完整的说明请阅读本章末尾的参考链接)

  • helperDialect:分页插件会自动检测当前的数据库连接,自动选择合适的分页方式。 你可以配置helperDialect属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:
    oracle, mysql, mariadb, sqlite, hsqldb, postgresql, db2, sqlserver, informix, h2, sqlserver2012, derby

  • pageSizeZero:默认值为 false,当该参数设置为 true 时,如果pageSize=0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。

如果觉得某个地方使用分页后,你仍然想通过控制参数查询全部的结果,你可以配置 pageSizeZero 为 true, 配置后,当 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果。

  • reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页,pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。

如果你分页插件使用于类似分页查看列表式的数据,如新闻列表,软件列表, 你希望用户输入的页数不在合法范围(第一页到最后一页之外)时能够正确的响应到正确的结果页面, 那么你可以配置 reasonable 为 true,这时如果 pageNum<=0 会查询第一页,如果 pageNum>总页数 会查询最后一页。

  • params:为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum, pageSize, count, pageSizeZero, reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。

  • closeConn:默认值为 true。当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。

如果你在 Spring 中配置了动态数据源,并且连接不同类型的数据库,这时你可以配置 autoRuntimeDialect 为 true,这样在使用不同数据源时,会使用匹配的分页进行查询。 这种情况下,你还需要特别注意 closeConn 参数,由于获取数据源类型会获取一个数据库连接,所以需要通过这个参数来控制获取连接后,是否关闭该连接。 默认为 true,有些数据库连接关闭后就没法进行后续的数据库操作。而有些数据库连接不关闭就会很快由于连接数用完而导致数据库无响应。所以在使用该功能时,特别需要注意你使用的数据源是否需要关闭数据库连接。

8.5 支持的物理分页

在编写本教程时,PageHelper 支持以下数据库的物理分页 PageAutoDialect:

static {
    //注册别名
    registerDialectAlias("hsqldb", HsqldbDialect.class);
    registerDialectAlias("h2", HsqldbDialect.class);
    registerDialectAlias("phoenix", HsqldbDialect.class);
    registerDialectAlias("postgresql", PostgreSqlDialect.class);
    registerDialectAlias("mysql", MySqlDialect.class); 
    registerDialectAlias("mariadb", MySqlDialect.class); 
    registerDialectAlias("sqlite", MySqlDialect.class);
    registerDialectAlias("herddb", HerdDBDialect.class);
    registerDialectAlias("oracle", OracleDialect.class); 
    registerDialectAlias("oracle9i", Oracle9iDialect.class); 
    registerDialectAlias("db2", Db2Dialect.class); 
    registerDialectAlias("informix", InformixDialect.class); 
    //解决 informix-sqli #129,仍然保留上面的 
    registerDialectAlias("informix-sqli", InformixDialect.class);
    registerDialectAlias("sqlserver", SqlServerDialect.class); 
    registerDialectAlias("sqlserver2012", SqlServer2012Dialect.class);
    registerDialectAlias("derby", SqlServer2012Dialect.class); 
    //达梦数据库,https://github.com/mybatis-book/book/issues/43 
    registerDialectAlias("dm", OracleDialect.class); 
    //阿里云PPAS数据库,https://github.com/pagehelper/Mybatis-PageHelper/issues/281 
    registerDialectAlias("edb", OracleDialect.class); 
    //神通数据库 
    registerDialectAlias("oscar", OscarDialect.class); 
    registerDialectAlias("clickhouse", MySqlDialect.class); 
    //瀚高数据库 
    registerDialectAlias("highgo", HsqldbDialect.class); 
    //虚谷数据库 
    registerDialectAlias("xugu", HsqldbDialect.class); 
}

如果你使用的数据库不在这个列表时,你可以配置 dialectAlias 参数。

最新的内容请查看官方说明

8.6 参考资料

编写本教程时 PageHelper 的最新版本是 5.3.0 ,下面这个链接是关于这个版本的更新内容
https://github.com/pagehelper/Mybatis-PageHelper/releases/tag/v5.3.0

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。