当前位置:   article > 正文

6.Mybatis分页插件(PageHelper),解决PageHelper.startPage()不安全分页

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

最新版本号可以从首页查看

(3)Spring Boot Starter(启动器)

//推荐使用下面这种方式
<!--pagehelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.3</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

(3)Spring Boot(application.properties)

#分页插件
pagehelper.helper-dialect=mysql
pagehelper.params=count=countSql
pagehelper.reasonable=true
pagehelper.support-methods-arguments=true
  • 1
  • 2
  • 3
  • 4
  • 5

分页插件参数介绍

分页插件提供了多个可选参数,这些参数使用时,按照上面两种配置方式中的示例配置即可

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
  • 1
(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);  
  • 1
  • 2

参数:
offset:偏移量
limit:每页展示多少数据

List<Student> list = studentMapper.find(new RowBounds(0, 10));
Page page = ((Page) list;

Page<Student>  page = studentMapper.find(new RowBounds(0, 10));
  • 1
  • 2
  • 3
  • 4

mappep.xml里面正常配置,不用对rowBounds任何操作。mybatis的拦截器自动操作rowBounds进行分页。使用RowBounds最大好处就是节省了在xml再拼装limit
总结:Mybatis的逻辑分页比较简单,简单来说就是取出所有满足条件的数据,然后舍弃掉前面offset条数据,然后再取剩下的数据的limit条

使用这种调用方式时,可以使用RowBounds参数进行分页,这种方式侵入性最小,可以看到,通过RowBounds方式调用只是使用了这个参数,并没有增加其他任何内容

分页插件检测到使用了RowBounds参数时,就会对该查询进行物理分页。关于这种方式的调用,有两个特殊的参数是针对RowBounds(场景一和场景二)

注意:不只有命名空间方式可以用RowBounds,使用接口的时候也可以增加RowBounds参数。

// 这种情况下也会进行物理分页查询
List<Country> selectAll(RowBounds rowBounds);  
  • 1
  • 2

注意:由于默认情况下的RowBounds无法获取查询总数,分页插件提供了一个继承自RowBounds的PageRowBounds,这个对象中增加了total属性,执行分页查询后,可以从该属性得到查询总数。


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

//start:开始处,rowNum:数目
PageHelper.offsetPage(int offset, int limit);
List<User> list = userMapper.selectAll();
  • 1
  • 2
  • 3

跟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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

(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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

当调用这个方法时,由于同时发现了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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

当从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));
  • 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

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>();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

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

这种写法就能保证安全

如果对此不放心,可以手动清理ThreadLocal存储的分页参数

List<User> list;
if (param != null) {
	PageHelper.startPage(1, 10);
	try {
		list = userMapper.selectAll(param);
	} finally {
		PageHelper.clearPage();
	}
} else {
	list = new ArrayList<User>();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这么写很不好看,而且没有必要。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/991913
推荐阅读
相关标签
  

闽ICP备14008679号