一、分页插件 Pagehelper

PageHelperMybatis的一个分页插件,非常好用!

1.1 Spring Boot 依赖

<!-- pagehelper 分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>

也可以这么引入

<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>latest version</version>
</dependency>

1.2 PageHelper 配置

配置文件增加PageHelper的配置,主要设置了分页方言和支持接口参数传递分页参数,如下:

pagehelper:
# 指定数据库
helper-dialect: mysql
# 默认是false。启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages(最大页数)会查询最后一页。禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据
reasonable: false
# 是否支持接口参数来传递分页参数,默认false
support-methods-arguments: true
# 为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
params: count=countSql
row-bounds-with-count: true

项目完整配置文件详见文mybatis-pagehelper

1.3 如何分页

只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会自动分页!!!!

@Test
public void selectForPage() {
// 第几页
int currentPage = 2;
// 每页数量
int pageSize = 5;
// 排序
String orderBy = "id desc";
PageHelper.startPage(currentPage, pageSize, orderBy);
List<UserInfoPagehelperDO> users = userInfoPagehelperMapper.selectList();
PageInfo<UserInfoPagehelperDO> userPageInfo = new PageInfo<>(users);
log.info("userPageInfo:{}", userPageInfo);
}
...: userPageInfo:PageInfo{pageNum=2, pageSize=5, size=1, startRow=6, endRow=6, total=6, pages=2, list=Page{count=true, pageNum=2, pageSize=5, startRow=5, endRow=10, total=6, pages=2, reasonable=false, pageSizeZero=false}[UserInfoPagehelperDO{id=1, userName='null', age=22, createTime=null}], prePage=1, nextPage=0, isFirstPage=false, isLastPage=true, hasPreviousPage=true, hasNextPage=false, navigatePages=8, navigateFirstPage=1, navigateLastPage=2, navigatepageNums=[1, 2]}

这里的返回结果包括数据、是否为第一页/最后一页、总页数、总记录数,详见Mybatis-PageHelper

Pagehelper 分页完整示例

二、Mybatis 拦截器实现分页

2.1 Mybatis 拦截器

Mybatis 官网【插件】部分有以下描述:

  1. 通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
  2. MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

即:我们可以通过拦截器的方式,实现MyBatis插件(数据分页)

接下来重点演示我如何使用了拦截器实现分页。

2.2 调用形式

在看如何实现之前,我们先看看如何使用:

照抄 PageHelper 的设计,先调用一个静态方法,对下面第一个方法的sql语句进行拦截,在new一个分页对象时自动处理。

@Test
public void selectForPage() {
// 该查询进行分页,指定第几页和每页数量
PageInterceptor.startPage(1,2);
List<UserInfoDO> all = dao.findAll();
PageResult<UserInfoDO> result = new PageResult<>(all);
// 分页结果打印
System.out.println("总记录数:" + result.getTotal());
System.out.println(result.getData().toString());
}

然后我们主要看看实现步骤。

2.3 数据库方言

定义好一个方言接口,不同的数据使用不同的方言实现

  • Dialect.java
public interface Dialect {
/**
* 获取count SQL语句
*
* @param targetSql
* @return
*/
default String getCountSql(String targetSql) {
return String.format("select count(1) from (%s) tmp_count", targetSql);
} /**
* 获取limit SQL语句
* @param targetSql
* @param offset
* @param limit
* @return
*/
String getLimitSql(String targetSql, int offset, int limit);
}
  • Mysql 分页方言
@Component
public class MysqlDialect implements Dialect{ private static final String PATTERN = "%s limit %s, %s"; private static final String PATTERN_FIRST = "%s limit %s"; @Override
public String getLimitSql(String targetSql, int offset, int limit) {
if (offset == 0) {
return String.format(PATTERN_FIRST, targetSql, limit);
} return String.format(PATTERN, targetSql, offset, limit);
}
}

2.4 拦截器核心逻辑

该部分完整代码见 PageInterceptor.java

  • 分页辅助参数内部类 PageParam.java
public static class PageParam {
// 当前页
int pageNum; // 分页开始位置
int offset; // 分页数量
int limit; // 总数
public int totalSize; // 总页数
public int totalPage;
}
  • 查询总记录数
private long queryTotal(MappedStatement mappedStatement, BoundSql boundSql) throws SQLException {

    Connection connection = null;
PreparedStatement countStmt = null;
ResultSet rs = null;
try { connection = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection(); String countSql = this.dialect.getCountSql(boundSql.getSql()); countStmt = connection.prepareStatement(countSql);
BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql,
boundSql.getParameterMappings(), boundSql.getParameterObject()); setParameters(countStmt, mappedStatement, countBoundSql, boundSql.getParameterObject()); rs = countStmt.executeQuery();
long totalCount = 0;
if (rs.next()) {
totalCount = rs.getLong(1);
} return totalCount;
} catch (SQLException e) {
log.error("查询总记录数出错", e);
throw e;
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
log.error("exception happens when doing: ResultSet.close()", e);
}
} if (countStmt != null) {
try {
countStmt.close();
} catch (SQLException e) {
log.error("exception happens when doing: PreparedStatement.close()", e);
}
} if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
log.error("exception happens when doing: Connection.close()", e);
}
}
}
}
  • 对分页SQL参数?设值
private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
Object parameterObject) throws SQLException {
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler.setParameters(ps);
}
  • 利用方言接口替换原始的SQL语句
private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType()); builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuffer keyProperties = new StringBuffer();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
} //setStatementTimeout()
builder.timeout(ms.getTimeout()); //setStatementResultMap()
builder.parameterMap(ms.getParameterMap()); //setStatementResultMap()
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType()); //setStatementCache()
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache()); return builder.build();
}
  • 计算总页数
public int countPage(int totalSize, int offset) {
int totalPageTemp = totalSize / offset;
int plus = (totalSize % offset) == 0 ? 0 : 1;
totalPageTemp = totalPageTemp + plus;
if (totalPageTemp <= 0) {
totalPageTemp = 1;
}
return totalPageTemp;
}
  • 供调用的静态分页方法

我这里设计的,页数是从1开始的,如果习惯用0开始,可以自己修改。

public static void startPage(int pageNum, int pageSize) {
int offset = (pageNum-1) * pageSize;
int limit = pageSize;
PageInterceptor.PageParam pageParam = new PageInterceptor.PageParam();
pageParam.offset = offset;
pageParam.limit = limit;
pageParam.pageNum = pageNum;
PARAM_THREAD_LOCAL.set(pageParam);
}

2.5 分页结果集

为了便于结果封装,我这里自己封装了一个比较全的分页结果集,包含太多的东西了,自己慢慢看下面的属性吧(自认为比较全了,欢迎打脸)

public class PageResult<T> implements Serializable {
/**
* 是否为第一页
*/
private Boolean isFirstPage = false;
/**
* 是否为最后一页
*/
private Boolean isLastPage = false;
/**
* 当前页
*/
private Integer pageNum;
/**
* 每页的数量
*/
private Integer pageSize;
/**
* 总记录数
*/
private Integer totalSize;
/**
* 总页数
*/
private Integer totalPage;
/**
* 结果集
*/
private List<T> data; public PageResult() {
} public PageResult(List<T> data) {
this.data = data;
PageInterceptor.PageParam pageParam = PageInterceptor.PARAM_THREAD_LOCAL.get();
if (pageParam != null) {
pageNum = pageParam.pageNum;
pageSize = pageParam.limit;
totalSize = pageParam.totalSize;
totalPage = pageParam.totalPage;
isFirstPage = (pageNum == 1);
isLastPage = (pageNum == totalPage);
PageInterceptor.PARAM_THREAD_LOCAL.remove();
}
} public Integer getPageNum() {
return pageNum;
} public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
} public Integer getPageSize() {
return pageSize;
} public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
} public Integer getTotalSize() {
return totalSize;
} public void setTotalSize(Integer totalSize) {
this.totalSize = totalSize;
} public Integer getTotalPage() {
return totalPage;
} public void setTotalPage(Integer totalPage) {
this.totalPage = totalPage;
} public List<T> getData() {
return data;
} public void setData(List<T> data) {
this.data = data;
} public Boolean getFirstPage() {
return isFirstPage;
} public void setFirstPage(Boolean firstPage) {
isFirstPage = firstPage;
} public Boolean getLastPage() {
return isLastPage;
} public void setLastPage(Boolean lastPage) {
isLastPage = lastPage;
} @Override
public String toString() {
return "PageResult{" +
"isFirstPage=" + isFirstPage +
", isLastPage=" + isLastPage +
", pageNum=" + pageNum +
", pageSize=" + pageSize +
", totalSize=" + totalSize +
", totalPage=" + totalPage +
", data=" + data +
'}';
}
}

2.6 简单测试下

@Test
public void selectForPage() {
// 该查询进行分页,指定第几页和每页数量
PageInterceptor.startPage(1,4);
List<UserInfoDO> all = userMapper.findAll();
PageResult<UserInfoDO> result = new PageResult<>(all);
// 分页结果打印
log.info("总记录数:{}", result.getTotalSize());
log.info("list:{}", result.getData());
log.info("result:{}", result);
}

使用方法基本1.3完全一致吧,只是封装成了我自己的分页结果集。

  • 日志如下:
....: ==>  Preparing: SELECT id, user_name, age, create_time FROM user_info_pageable limit 4
....: ==> Parameters:
....: <== Total: 4
....: 总记录数:6
....: list:[UserInfoDO(id=1, userName=张三, age=22, createTime=2019-10-08T20:52:46), UserInfoDO(id=2, userName=李四, age=21, createTime=2019-12-23T20:22:54), UserInfoDO(id=3, userName=王二, age=22, createTime=2019-12-23T20:23:15), UserInfoDO(id=4, userName=马五, age=20, createTime=2019-12-23T20:23:15)]
....: result:PageResult{isFirstPage=true, isLastPage=false, pageNum=1, pageSize=4, totalSize=6, totalPage=2, data=[UserInfoDO(id=1, userName=张三, age=22, createTime=2019-10-08T20:52:46), UserInfoDO(id=2, userName=李四, age=21, createTime=2019-12-23T20:22:54), UserInfoDO(id=3, userName=王二, age=22, createTime=2019-12-23T20:23:15), UserInfoDO(id=4, userName=马五, age=20, createTime=2019-12-23T20:23:15)]}

通过日志分析,发现普通的SELECT * FROM user_info_pageable 被重新组装成SELECT * FROM user_info_pageable limit 4,说明拦截器实现的分页成功。

三、总结

两种方式:Pagehelper 分页和自己实现,根据实际情况自己选用吧。

  1. Pagehelper 分页示例代码
  2. Mybatis 拦截器实现分页示例代码

3.1 日常求赞

  1. 祖传秘籍 Spring Boot 葵花宝典 开源中,欢迎前来吐槽,提供线索!
  2. 九阳神功 【Java 知识笔记本】 开源中,欢迎前来吐槽,提供线索!

3.2 文化交流

  1. 风尘博客
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. 风尘博客-CSDN
  5. Github

Mybatis 分页:Pagehelper + 拦截器实现的更多相关文章

  1. MyBatis 分页之拦截器实现

    分页是WEB程序中常见的功能,mybatis分页实现与hibernate不同,相比hibernate,mybatis实现分页更为麻烦.mybatis实现分页需要自己编写(非逻辑分页RowBounds) ...

  2. MyBatis功能点二:MyBatis提供的拦截器平台

    前面关于MyBatis功能点二plugin已经介绍了一些应用及其实现的底层代码,本文总结MyBatis提供的拦截器平台框架体系. 通过MyBatis功能点二:从责任链设计模式的角度理解插件实现技术 - ...

  3. Mybatis中的拦截器

    作者:moshenglv的专栏 拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法. ...

  4. Mybatis自定义SQL拦截器

    本博客介绍的是继承Mybatis提供的Interface接口,自定义拦截器,然后将项目中的sql拦截一下,打印到控制台. 先自定义一个拦截器 package com.muses.taoshop.com ...

  5. MyBatis空where拦截器

    最近项目中出现了至少两次因为Mybatis的动态where条件不满足导致实际sql语句的where条件为空,进而查询全表,当数据量比较大的时候,导致OOM的情况. 如何禁止这种情况,个人觉得三种措施: ...

  6. Mybatis Plugin(拦截器)的开发

    1.Plugin   MyBatis 允许使用插件来拦截的方法调用包括: • Executor (update, query, flushStatements, commit, rollback, g ...

  7. Mybatis那些事-拦截器(Plugin+Interceptor)

    作者:yhjyumi的专栏 数据权限实现(Mybatis拦截器+JSqlParser) Mybatis的拦截器实现机制,使用的是JDK的InvocationHandler. 当我们调用Paramete ...

  8. mybatis分页 -----PageHelper插件

    对查询结果进行分页 一,使用limit进行分页 1.mybatis 的sql语句: <if test="page !=null and rows !=null"> li ...

  9. mybatis分页PageHelper插件的使用

    1.jar包, 2.改mybatis的配置文件,加上这段配置: <plugins><plugin interceptor="com.github.pagehelper.Pa ...

随机推荐

  1. Git把本地代码推送到远程github仓库

    运用Git版本控制系统进行代码的管理,以便于团队成员的协作,由于之前是使用svn来进行版本控制,所以对于Git使用还有待熟练掌握.Git与svn类似,个人认为两者之间比较直观的区别就是 Git 不需要 ...

  2. java面试题(一年工作经验)的心得

    看面试题 正常人第一步肯定都会看面试题,我也不例外,在看的过程中,我发现有些文章写的不错,对我帮助不小值得推荐,如下: Java面试题全集(上) 很多基础的东西,建议先看. 各大公司Java后端开发面 ...

  3. VMware15 安装Mac 10.14系统/苹果系统

    安装环境 Windows专业版 VMware Workstation Pro 15 所需资源 VMware Workstation Pro 15.0.0 Build 10134415 官网下载地址:h ...

  4. AI-web-1靶机过关记录

    靶机地址:172.16.1.195 Kali地址:172.16.1.107 1.信息收集 端口扫描: 目录扫描: 发现robots.txt敏感文件,查看 存在/m3diNf0/,/se3reTdir7 ...

  5. 彻底解决Python编码问题

    1. 基本概念 字符集(Character set) 解释:文字和符合的总称 常见字符集: Unicode字符集 ASCII字符集(Unicode子集) GB2312字符集 编码方法(Encoding ...

  6. Java 网络编程 --基于UDP实现一对一聊天功能

    UDP 基本流程: UDP发送端基本流程: 1.使用DatagramSocket 指定端口 创建发送端 2.准备数据 一定转成字节数组 3. 封装成DatagramPacket 包裹,需要指定目的地 ...

  7. echarts迁徙图

    前段时间在echarts社区,看见别人写的echarts迁徙图,学习并也写了一个 预览地址: https://gallery.echartsjs.com/editor.html?c=xYS-YtzOa ...

  8. 9个小技巧让你的 if else看起来更优雅

    if else 是我们写代码时,使用频率最高的关键词之一,然而有时过多的 if else 会让我们感到脑壳疼,例如下面这个伪代码: 是不是很奔溃?虽然他是伪代码,并且看起来也很夸张,但在现实中,当我们 ...

  9. 如何给 Visual Studio 的输出程序添加版本信息

    出处:https://stackoverflow.com/questions/284258/how-do-i-set-the-version-information-for-an-existing-e ...

  10. Python获取当前 年 月 日

    import datetime datetime.datetime.now().year datetime.datetime.now().month datetime.datetime.now().d ...