郭卫东 发表于 2024-8-7 20:46:22

6.Mybatis分页插件(PageHelper),办理PageHelper.startPage()不安全分页

目录

Mybatis专栏目录(点击进入…)
@TOC
Mybatis分页插件(PageHelper)

1.引入分页插件(依靠/Jar)

引入分页插件有下面2种方式,推荐利用Maven方式
(1)引入Jar包

可以从下面的地点中下载最新版本的jar包
https://oss.sonatype.org/content/repositories/releases/com/github/pagehelper/pagehelper/
http://repo1.maven.org/maven2/com/github/pagehelper/pagehelper/
由于利用了sql解析工具,还需要下载jsqlparser.jar:
http://repo1.maven.org/maven2/com/github/jsqlparser/jsqlparser/0.9.5/
(2)利用Maven在pom.xml中添加如下依靠。此版本依靠mybatis 3.4.6

<dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.1.8</version>
</dependency>
<!-- 编译依赖项 -->
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>1.2</version>
</dependency>
最新版本号可以从首页查看
(3)Spring Boot Starter(启动器)

//推荐使用下面这种方式
<!--pagehelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.3</version>
</dependency>

2.配置拦截器插件

特别注意,新版拦截器是com.github.pagehelper.PageInterceptor
com.github.pagehelper.PageHelper现在是一个特别的dialect实现类,是分页插件的默认实现类,提供了和从前相同的用法
(1)在MyBatis焦点配置文件(Xml)中配置拦截器插件

<plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
                <property name="helperDialect" value="mysql" />
        </plugin>
</plugins>
plugins在配置文件中的位置必须符合要求(次序),否则会报错
次序如下:
①properties
②settings
③typeAliases
④typeHandlers
⑤objectFactory
⑥objectWrapperFactory
⑦plugins
⑧environments
⑨databaseIdProvider
⑩mapper

(2)在Spring配置文件中配置拦截器插件

利用Spring的属性配置方式,可以利用plugins属性像下面这样配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 注意其他配置 -->
        <property name="plugins">
                <array>
                        <bean class="com.github.pagehelper.PageInterceptor">
                                <property name="properties">
                                        <!--使用下面的方式配置参数,一行配置一个 -->
                                        <value>
                                                <!--使用的数据库类型 -->
                                                helperDialect=mysql
                                                reasonable=true
                                                supportMethodsArguments=true
                                                params=count=countSql
                                                autoRuntimeDialect=true
                                        </value>
                                </property>
                        </bean>
                </array>
        </property>
</bean>

(3)Spring Boot(application.properties)

#分页插件
pagehelper.helper-dialect=mysql
pagehelper.params=count=countSql
pagehelper.reasonable=true
pagehelper.support-methods-arguments=true

分页插件参数介绍

分页插件提供了多个可选参数,这些参数利用时,按照上面两种配置方式中的示例配置即可
dialect:默认环境下会利用PageHelper方式进行分页,假如想要实现本身的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect)接口,然后配置该属性为实现类的全限定名称
下面几个参数都是针对默认dialect环境下的参数。利用自界说dialect实现时,下面的参数没有任何作用
(1)helperDialect:数据库方言

分页插件会自动检测当前的数据库链接,自动选择符合的分页方式。可以配置helperDialect属性来指定分页插件利用哪种方言
配置时,可以利用下面的缩写值:
oracle、mysql、mariadb、sqlite、hsqldb、postgresql、db2
sqlserver、informix、h2、sqlserver2012、derby
特别注意:利用Sql Server 2012数据库时,需要手动指定为Sql Server 2012,否则会利用Sql Server 2005的方式进行分页。也可以实现AbstractHelperDialect,然后配置该属性为实现类的全限定名称即可利用自界说的实现方法。
(2)offsetAsPageNum:RowBounds的offset作pageNum

该参数对利用RowBounds作为分页参数时有效。当该参数设置为true时,会将RowBounds中的offset参数当成pageNum利用,可以用页码和页面大小两个参数进行分页(默认值为false)
(3)rowBoundsWithCount:RowBounds进行count查询

该参数对利用RowBounds作为分页参数时有效。当该参数设置为true时,利用RowBounds分页会进行count查询(默认值为false)
(4)pageSizeZero:查询全部效果

当该参数设置为true时。假如pageSize = 0或者RowBounds.limit = 0就会查询出全部的效果(相称于没有执行分页查询,但是返回效果仍然是Page类型)(默认值为false)
(5)reasonable:查询第一页、末了一页、参数查询

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

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

支持通过Mapper接口参数来通报分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面params配置的字段中取值,查找到符合的值时就会自动分页。利用方法可以参考测试代码中的com.github.pagehelper.test.basic包下的ArgumentsMapTest和ArgumentsObjTest
(8)autoRuntimeDialect

设置为true时,允许在运行时根据多数据源自动识别对应方言的分页(不支持自动选择sqlserver2012,只能利用sqlserver),用法和注意事项参考下面的场景五(默认值为 false)
(9)closeConn:是否关闭数据库连接

当利用运行时动态数据源或没有设置helperDialect属性自动获取数据库类型时,会自动获取一个数据库连接,通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为false后,不会关闭获取的连接,这个参数的设置要根据本身选择的数据源来决定(默认值为true)
紧张提示:
当offsetAsPageNum=false的时候,由于PageNum题目,RowBounds查询的时候reasonable会逼迫为false。利用PageHelper.startPage()不受影响

怎样选择配置这些参数

场景一

假如仍然在用雷同Mybatis式的定名空间调用方式,也许会用到rowBoundsWithCount,分页插件对RowBounds支持和MyBatis默认的方式是一致,默认环境下不会进行count查询,假如想在分页查询时进行count查询,以及利用更强大的PageInfo类,需要设置该参数为true
注意:PageRowBounds想要查询总数也需要配置该属性为true
场景二

假如仍然在用雷同Mybatis式的定名空间调用方式,RowBounds中的两个参数offset、limit不如 pageNum、pageSize容易理解,可以利用offsetAsPageNum参数,将该参数设置为true后,offset会当成pageNum利用,limit和pageSize含义相同
场景三

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

假如分页插件利用于雷同分页查看列表式的数据。如新闻列表、软件列表,盼望用户输入的页数不在合法范围(第一页到末了一页之外)时能够精确的响应到精确的效果页面,那么可以配置reasonable为true,这时假如pageNum<=0会查询第一页,假如pageNum>总页数会查询末了一页
场景五

假如在Spring中配置了动态数据源。而且连接不同类型的数据库,这时可以配置 autoRuntimeDialect为true,这样在利用不同数据源时,会利用匹配的分页进行查询。这种环境下,还需要特别注意closeConn参数,由于获取数据源类型会获取一个数据库连接,所以需要通过这个参数来控制获取连接后,是否关闭该连接。 默认为true,有些数据库连接关闭后就没法进行后续的数据库利用。而有些数据库连接不关闭就会很快由于连接数用完而导致数据库无响应。所以在利用该功能时,特别需要注意你利用的数据源是否需要关闭数据库连接。
当不利用动态数据源而只是自动获取 helperDialect 时,数据库连接只会获取一次,所以不需要担心占用的这一个连接是否会导致数据库出错,但是最好也根据数据源的特性选择是否关闭连接

分页注意

(1)PageHelper.startPage()紧张提示

只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会被分页
(2)不要配置多个分页插件

请不要在系统中配置多个分页插件(利用Spring时。mybatis-config.xml和Spring配置方式,选择其中一种,不要同时配置多个分页插件)
(3)分页插件不支持带有for update语句的分页

对于带有for update的sql,会抛出运行时异常,对于这样的sql建议手动分页,究竟这样的sql需要器重
(4)分页插件不支持嵌套效果映射

由于嵌套效果方式会导致效果集被折叠,因此分页查询的效果在折叠后总数会减少,所以无法保证分页效果数量精确

3.常用分页方式详细

(1)RowBounds方式的调用(mybatis)

Mybatis提供了一个简朴的逻辑分页利用类RowBounds,在DefaultSqlSession提供的某些查询接口中我们可以看到RowBounds是作为参数用来进行分页,逻辑分页会将全部的效果都查询到,然后根据RowBounds中提供的offset和limit值来获取末了的效果。
RowBounds();
RowBounds(int offset, int limit);
参数:
offset:偏移量
limit:每页展示多少数据
List<Student> list = studentMapper.find(new RowBounds(0, 10));
Page page = ((Page) list;

Page<Student>page = studentMapper.find(new RowBounds(0, 10));
mappep.xml内里正常配置,不消对rowBounds任何利用。mybatis的拦截器自动利用rowBounds进行分页。利用RowBounds最大利益就是节省了在xml再拼装limit
总结:Mybatis的逻辑分页比力简朴,简朴来说就是取出全部满意条件的数据,然后舍弃掉前面offset条数据,然后再取剩下的数据的limit条
利用这种调用方式时,可以利用RowBounds参数进行分页,这种方式侵入性最小,可以看到,通过RowBounds方式调用只是利用了这个参数,并没有增长其他任何内容
分页插件检测到利用了RowBounds参数时,就会对该查询进行物理分页。关于这种方式的调用,有两个特别的参数是针对RowBounds(场景一和场景二)
注意:不光有定名空间方式可以用RowBounds,利用接口的时候也可以增长RowBounds参数。
// 这种情况下也会进行物理分页查询
List<Country> selectAll(RowBounds rowBounds);
注意:由于默认环境下的RowBounds无法获取查询总数,分页插件提供了一个继续自RowBounds的PageRowBounds,这个对象中增长了total属性,执行分页查询后,可以从该属性得到查询总数。

(2)PageHelper.offsetPage()静态方法

//start:开始处,rowNum:数目
PageHelper.offsetPage(int offset, int limit);
List<User> list = userMapper.selectAll();
跟startPage()利用一样,只是限定、参数不同。
(3)PageHelper.startPage静态方法调用(推荐)

除了PageHelper.startPage方法外,还提供了雷同用法的PageHelper.offsetPage方法
在需要进行分页的MyBatis查询方法前调用PageHelper.startPage静态方法即可,紧跟在这个方法后的第一个MyBatis查询方法会被进行分页
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页,后面的不会被分页,除非再次调用PageHelper.startPage()
List<User> list = userMapper.selectAll();

//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>或者使用PageInfo类包装
// ①Page
Page page = (Page) list;
page.getTotal();

// ②PageInfo
PageInfo page = new PageInfo(list);
page.getTotal();

(4)利用参数方法方式

想要利用参数方式,需要配置supportMethodsArguments参数为 true,同时要配置params参数。
<plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
      <!-- 配置supportMethodsArguments=true -->
                <property name="supportMethodsArguments" value="true" />
                <property name="params"
                        value="pageNum=pageNumKey;pageSize=pageSizeKey;" />
        </plugin>
</plugins>
在MyBatis Mapper方法中
List<Country> selectByPageNumSize(
        @Param("user") User user,
        @Param("pageNumKey") int pageNum,
        @Param("pageSizeKey") int pageSize);

// 在代码中直接调用:
List<User> userList = userMapper.selectByPageNumSize(user, 1, 10);
当调用这个方法时,由于同时发现了pageNumKey和pageSizeKey参数,这个方法就会被分页。params 提供的几个参数都可以这样利用
(5)参数对象方式

除了上面这种方式外,假如 User 对象中包含这两个参数值,也可以有下面的方法:
// 如果pageNum和pageSize存在于User对象中,只要参数有值,也会被分页
public class User {
        // 其他fields
        // 下面两个参数名和params配置的名字一致
        private Integer pageNum;
        private Integer pageSize;
}

// 存在以下Mapper接口方法,不需要在xml处理后两个参数
public interface UserMapper {
        List<User> selectByPageNumSize(User user);
}

// 当user中的pageNum!=null&&pageSize!=null时,会自动分页
List<User> listUser = userMapper.selectByPageNumSize(user);
当从User中同时发现了pageNum Key和pageSize Key参数,这个方法就会被分页
注意:pageNum和pageSize两个属性同时存在才会触发分页利用,在这个前提下,其他的分页参数才会生效。
(6)ISelect接口方式

//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
    @Override
    public void doSelect() {
      countryMapper.selectGroupBy();
    }
});

//jdk8 lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());

//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
    @Override
    public void doSelect() {
      countryMapper.selectGroupBy();
    }
});

//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy());

//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
    @Override
    public void doSelect() {
      countryMapper.selectLike(country);
    }
});

//lambda
total = PageHelper.count(()->countryMapper.selectLike(country));

PageHelper安全调用

1.利用RowBounds和PageRowBounds参数方式是极其安全的

2.利用参数方式是极其安全的

3.利用ISelect接口调用是极其安全的

ISelect接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的count查询方式,这个方法可以将任意的查询方法,变成一个select count(*)的查询方法。

PageHelper.startPage()什么时候会导致不安全的分页?办理?

PageHelper.startPage()方法利用了静态的ThreadLocal参数,分页参数和线程是绑定的。只要可以保证在PageHelper方法调用后紧跟MyBatis查询方法,就是安全的。因为PageHelper在finally代码段中自动扫除了ThreadLocal存储的对象
假如代码在进入Executor前发生异常,就会导致线程不可用,这属于人为的Bug(例如接口方法和XML中的不匹配,导致找不到MappedStatement 时),这种环境由于线程不可用,也不会导致ThreadLocal参数被错误的利用。
(1)不安全的用法

PageHelper.startPage(1, 10);
List<User> list;
if (param != null) {
        list = userMapper.selectAll(param);
} else {
        list = new ArrayList<User>();
}
这种环境下由于param存在null的环境,就会导致PageHelper生产了一个分页参数,但是没有被消费,这个参数就会一直保存在这个线程上。当这个线程再次被利用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:
List<User> list;
if (param != null) {
        PageHelper.startPage(1, 10);
        list = userMapper.selectAll(param);
} else {
        list = new ArrayList<User>();
}
这种写法就能保证安全
假如对此不放心,可以手动清理ThreadLocal存储的分页参数
List<User> list;
if (param != null) {
        PageHelper.startPage(1, 10);
        try {
                list = userMapper.selectAll(param);
        } finally {
                PageHelper.clearPage();
        }
} else {
        list = new ArrayList<User>();
}
这么写很不好看,而且没有必要。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 6.Mybatis分页插件(PageHelper),办理PageHelper.startPage()不安全分页