MyBatis系列之分页插件及问题

2024-06-24 08:44

本文主要是介绍MyBatis系列之分页插件及问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概述

无论是C端产品页面,还是后台系统页面,不可能一次性将全部数据加载出来。后台系统一般都是PC端登录,用Table组件(如Ant Design Table)渲染展示数据,可点击列表的下一页(或指定某一页)查看数据。C端产品如App,在下滑时可查看更多数据,看起来像是一次性加载数据,实际上也是分批请求后台系统获取数据。而这,就是分页功能。

如果没有使用Hibernate或MyBatis这样的ORM工具,假如面对的是MySQL数据库,则可考虑自己拼接SQL,在末尾加上LIMIT M OFFSET N,有两种写法:

select * from order LIMIT 1, 3;
select * from order LIMIT 3 OFFSET 1;

解释:

  • limit后面跟两个参数时,第一个数表示要跳过的数量,后一位表示要取的数量
  • OFFSET表示要跳过的数量,LIMIT表示要取的数量

问题

大部分人几乎不会看到上面这种分页查询数据的写法,因为这种写法存在性能问题。如果表的数据量级只有几万或十几万,单表查询的性能损耗几乎可省略不计。但如果面对单表高达百万级别的数据量时,上面这种写法的执行耗时就不能忽略不计。

为了实现分页,每次收到分页请求时,数据库都需要进行低效的全表遍历。全表遍历,就是根据双向链表把磁盘上的数据页加载到磁盘的缓存页里去,然后在缓存页内部查找那条数据。

解决方案:

  1. 增加where条件,并且在where条件里使用索引过滤掉无关数据,即想要通过OFFSET跳过的数据,一般都是where id > 3000000
  2. 增加order by子句并确保order by的字段上有索引,这样可利用索引进行排序,而不是在内存中对所有行进行排序;
  3. 使用覆盖索引优化:SELECT * FROM order a INNER JOIN (SELECT id FROM order LIMIT 3000000, 20) b USING (id);

总之,可通过MySQL explain命令验证一下SQL。如果索引被使用,输出中的type列通常会显示range、ref或index等,而不是ALL(表示全表扫描)。

使用MyBatis后时,如何分页查询数据?一般会考虑使用分页插件,主要有以下3个(实际上远远不止这3个,记得刚工作时还用过一个MyBatis-Pagination,在Maven里搜索不到,不是下面列出的MyBatis-Paginator):

PageHelper

支持多种数据库(如MySQL、PostgreSQL、Oracle等)且配置简单,支持多数据库、自动分页、分页参数合理化、分页插件链式调用、自定义count查询。开源GitHub。

对应的Maven依赖为:

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

有提供对应的spring-boot-starter,依赖如下:

<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>2.1.0</version>
</dependency>

pagehelper-spring-boot-starter某个版本具体使用什么版本的pagehelper,可通过IDEA查看pom文件得知。

一定要知道**-spring-boot-starter实际上引用的还是**

简单使用:

// 省略其他非分页插件相关的import语句,下同
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;public void query() {SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession();try {MyMapper mapper = session.getMapper(MyMapper.class);PageHelper.startPage(1, 10);List<MyObject> list = mapper.selectMyObjects();PageInfo<MyObject> pageInfo = new PageInfo<>(list);} finally {session.close();}
}

MyBatis-Plus

MyBatis的一个增强工具包,提供许多开箱即用的功能(除mybatis-plus外无需额外引入其他依赖),包括分页。目标是简化开发,提高生产力。支持自动分页、多数据库、代码生成器,提供丰富的条件构造器。开源GitHub。

对应的Maven依赖最新版为:

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus</artifactId><version>3.5.7</version>
</dependency>

有提供对应的spring-boot-starter,依赖如下:

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.7</version>
</dependency>

可知,mybatis-plus-boot-startermybatis-plus保持同步更新和版本发布。

需要配置@Bean方法:

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;@Configuration
public class MyBatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {return new PaginationInterceptor();}
}

高版本的配置方法:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;
}

简单使用:

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;@Service
public class MyService {@Autowiredprivate MyMapper myMapper;public IPage<MyObject> getMyObjects(int page, int size) {Page<MyObject> myPage = new Page<>(page, size);return myMapper.selectPage(myPage, new QueryWrapper<>());}
}

MyBatis-Paginator

对应的Maven依赖为:

<dependency><groupId>com.github.miemiedev</groupId><artifactId>mybatis-paginator</artifactId><version>1.2.17</version>
</dependency>

开源GitHub。
简单使用:

import com.github.miemiedev.mybatis.paginator.domain.PageBounds;
import com.github.miemiedev.mybatis.paginator.domain.PageList;public void example() {SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession();try {MyMapper mapper = session.getMapper(MyMapper.class);PageBounds pageBounds = new PageBounds(1, 10);List<MyObject> list = mapper.selectMyObjects(pageBounds);PageList<MyObject> pageList = (PageList<MyObject>) list;} finally {session.close();}
}

选型

插件\对比项最后发布时间ForkStar
PageHelper2023.12.163.1k12.1k
MyBatis-Plus2024.06.104.2k16
MyBatis-Paginator2015.05.07218368

如果项目使用的是MyBatis,则可考虑使用PageHelper。
如果项目使用的是MyBatis-Plus,则可直接使用自带的分页功能。

原理

面试时可能会遇到的一个问题,MyBatis-Plus(或PageHelper)的实现原理是什么?

MybatisPlus基于MyBatis物理分页

以MyBatis-Plus低版本为例,分析分页原理的入口类是PaginationInterceptor,核心方法是:

public Object intercept(Invocation invocation) throws Throwable {StatementHandler statementHandler = (StatementHandler)PluginUtils.realTarget(invocation.getTarget());MetaObject metaObject = SystemMetaObject.forObject(statementHandler);this.sqlParser(metaObject);MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");// 只考虑SELECTif (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {return invocation.proceed();} else {RowBounds rowBounds = (RowBounds)metaObject.getValue("delegate.rowBounds");// 不需要分页if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {if (!this.localPage) {return invocation.proceed();}// 从ThreadLocal获取本地线程PaginationrowBounds = PageHelper.getPagination();if (rowBounds == null) {return invocation.proceed();}}BoundSql boundSql = (BoundSql)metaObject.getValue("delegate.boundSql");String originalSql = boundSql.getSql();Connection connection = (Connection)invocation.getArgs()[0];DBType dbType = StringUtils.isNotEmpty(this.dialectType) ? DBType.getDBType(this.dialectType) : JdbcUtils.getDbType(connection.getMetaData().getURL());// Pagination是RowBounds的子类if (rowBounds instanceof Pagination) {Pagination page = (Pagination)rowBounds;boolean orderBy = true;// searchCount默认为trueif (page.isSearchCount()) {// SqlInfo sqlInfo = SqlUtils.getOptimizeCountSql(page.isOptimizeCountSql(), this.sqlParser, originalSql);orderBy = sqlInfo.isOrderBy();this.queryTotal(this.overflowCurrent, sqlInfo.getSql(), mappedStatement, boundSql, page, connection);// 可等价替换为==0L,应该不存在<0的情况?if (page.getTotal() <= 0L) {return invocation.proceed();}}// 构建SQL追加order by子句String buildSql = SqlUtils.concatOrderBy(originalSql, page, orderBy);originalSql = DialectFactory.buildPaginationSql(page, buildSql, dbType, this.dialectClazz);} else {originalSql = DialectFactory.buildPaginationSql((RowBounds)rowBounds, originalSql, dbType, this.dialectClazz);}metaObject.setValue("delegate.boundSql.sql", originalSql);// 禁用内存分页,内存分页会查询所有结果出来处理,如果结果变化频繁这个数据还会不准metaObject.setValue("delegate.rowBounds.offset", 0);metaObject.setValue("delegate.rowBounds.limit", Integer.MAX_VALUE);return invocation.proceed();}
}

高版本的拦截器类是PaginationInnerInterceptor,比PaginationInterceptor要复杂一些,核心方法有两个:

/*** 进行count,如果count为0返回false,不再执行sql*/
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {// 都是先解析Page信息IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);if (page == null || page.getSize() < 0 || !page.searchCount() || resultHandler != Executor.NO_RESULT_HANDLER) {return true;}BoundSql countSql;MappedStatement countMs = buildCountMappedStatement(ms, page.countId());if (countMs != null) {countSql = countMs.getBoundSql(parameter);} else {countMs = buildAutoCountMappedStatement(ms);String countSqlStr = autoCountSql(page, boundSql.getSql());PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());}// 缓存CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);long total = 0;if (CollectionUtils.isNotEmpty(result)) {// 个别数据库 count 没数据不会返回 0Object o = result.get(0);if (o != null) {total = Long.parseLong(o.toString());}}page.setTotal(total);return continuePage(page);
}@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);if (null == page) {return;}// 处理 orderBy 拼接boolean addOrdered = false;String buildSql = boundSql.getSql();List<OrderItem> orders = page.orders();if (CollectionUtils.isNotEmpty(orders)) {addOrdered = true;buildSql = this.concatOrderBy(buildSql, orders);}// size 小于 0 且不限制返回值则不构造分页sqlLong _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;if (page.getSize() < 0 && null == _limit) {if (addOrdered) {PluginUtils.mpBoundSql(boundSql).sql(buildSql);}return;}// 最大为_limithandlerLimit(page, _limit);// 解析dialectIDialect dialect = findIDialect(executor);// 核心方法,根据各个Dialect构建SQLDialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);List<ParameterMapping> mappings = mpBoundSql.parameterMappings();Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();final Configuration configuration = ms.getConfiguration();model.consumers(mappings, configuration, additionalParameter);mpBoundSql.sql(model.getDialectSql());mpBoundSql.parameterMappings(mappings);
}

伏笔

个人猜测:很多开发者(和团队)最开始使用MyBatis,并使用PageHelper分页插件。后发现MyBatis-Plus确实比MyBatis好用,于是迁移到MP,形成MP+PageHelper共存的局面。在MP框架下PageHelper插件依然可以正常使用(有条件)。鄙人已经在至少两个公司的项目团队里看到这种混杂使用的情况:
在这里插入图片描述

问题

多依赖

后台系统,有一个列表页,点击第2页,没有响应。F12查看Chrome Console,控制台没有报错,说明不是前端JS报错。查看接口responseBody,发现分页有问题,nextPage=0:
在这里插入图片描述
后端分页有问题,对应的分页代码片段:

public String strategyList(JSONObject jsonObject) {PageHelper.startPage(Integer.parseInt(jsonObject.get("pageNo") + ""), Integer.parseInt(jsonObject.get("pageSize") + ""));list = channelPublicStrategyMapper.strategyList(jsonObject);PageInfo<Map> pageInfo = new PageInfo<>(list);return JSONObject.toJSONString(ServiceUtil.returnSuccessData(pageInfo));
}

看不出任何问题。调试,入参jsonObject.get("pageSize") == 10,前端传参没问题,但是最后返回的pageInfo包装信息不对劲:
在这里插入图片描述
到此时还是一脸懵逼。。

后来无意中点到源码,才发现PageHelperPageInfo不是同一个依赖包的API:
在这里插入图片描述
同事随手提交的代码,这不坑人么?

反思

推荐在同一个项目中,只选用一种分页方式,统一代码风格。

参考

这篇关于MyBatis系列之分页插件及问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089667

相关文章

MyBatis中$与#的区别解析

《MyBatis中$与#的区别解析》文章浏览阅读314次,点赞4次,收藏6次。MyBatis使用#{}作为参数占位符时,会创建预处理语句(PreparedStatement),并将参数值作为预处理语句... 目录一、介绍二、sql注入风险实例一、介绍#(井号):MyBATis使用#{}作为参数占位符时,会

mybatis执行insert返回id实现详解

《mybatis执行insert返回id实现详解》MyBatis插入操作默认返回受影响行数,需通过useGeneratedKeys+keyProperty或selectKey获取主键ID,确保主键为自... 目录 两种方式获取自增 ID:1. ​​useGeneratedKeys+keyProperty(推

java使用protobuf-maven-plugin的插件编译proto文件详解

《java使用protobuf-maven-plugin的插件编译proto文件详解》:本文主要介绍java使用protobuf-maven-plugin的插件编译proto文件,具有很好的参考价... 目录protobuf文件作为数据传输和存储的协议主要介绍在Java使用maven编译proto文件的插件

MyBatis-Plus 中 nested() 与 and() 方法详解(最佳实践场景)

《MyBatis-Plus中nested()与and()方法详解(最佳实践场景)》在MyBatis-Plus的条件构造器中,nested()和and()都是用于构建复杂查询条件的关键方法,但... 目录MyBATis-Plus 中nested()与and()方法详解一、核心区别对比二、方法详解1.and()

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

Redis出现中文乱码的问题及解决

《Redis出现中文乱码的问题及解决》:本文主要介绍Redis出现中文乱码的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 问题的产生2China编程. 问题的解决redihttp://www.chinasem.cns数据进制问题的解决中文乱码问题解决总结

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机

Golang如何用gorm实现分页的功能

《Golang如何用gorm实现分页的功能》:本文主要介绍Golang如何用gorm实现分页的功能方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录背景go库下载初始化数据【1】建表【2】插入数据【3】查看数据4、代码示例【1】gorm结构体定义【2】分页结构体

全面解析MySQL索引长度限制问题与解决方案

《全面解析MySQL索引长度限制问题与解决方案》MySQL对索引长度设限是为了保持高效的数据检索性能,这个限制不是MySQL的缺陷,而是数据库设计中的权衡结果,下面我们就来看看如何解决这一问题吧... 目录引言:为什么会有索引键长度问题?一、问题根源深度解析mysql索引长度限制原理实际场景示例二、五大解决