MyBatis系列之分页插件及问题

news2024/11/19 1:40:11

概述

无论是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 {
	@Bean
	public 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 {
	@Autowired
	private 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");
	// 只考虑SELECT
	if (!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获取本地线程Pagination
			rowBounds = 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默认为true
			if (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 没数据不会返回 0
        Object 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 且不限制返回值则不构造分页sql
    Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;
    if (page.getSize() < 0 && null == _limit) {
        if (addOrdered) {
            PluginUtils.mpBoundSql(boundSql).sql(buildSql);
        }
        return;
    }
    // 最大为_limit
    handlerLimit(page, _limit);
    // 解析dialect
    IDialect dialect = findIDialect(executor);
    // 核心方法,根据各个Dialect构建SQL
    DialectModel 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:
在这里插入图片描述
同事随手提交的代码,这不坑人么?

反思

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

参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1856840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“移”起AI+丨首创AI值守无人小店!中国移动视觉大模型加速落地

在城市里开设一家24小时便利店有多难&#xff1f;创业者常常面临着熬夜看店、全年无休的困境&#xff0c;而选择增加雇佣员工看店又会面临着成本高昂、利润微薄的问题。 日前在温州&#xff0c;一家AI无人值守便利店引发关注。在这家无人便利小店内&#xff0c;浙江移动试点部…

AI赋能天气:微软研究院发布首个大规模大气基础模型Aurora

编者按&#xff1a;气候变化日益加剧&#xff0c;高温、洪水、干旱&#xff0c;频率和强度不断增加的全球极端天气给整个人类社会都带来了难以估计的影响。这给现有的天气预测模型提出了更高的要求——这些模型要更准确地预测极端天气变化&#xff0c;为政府、企业和公众提供更…

头歌——机器、深度学习——图像生成

第1关&#xff1a;手写数字体生成 任务描述 本关任务&#xff1a;编写一个程序&#xff0c;实现手写数字体的生成。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.生成器&#xff0c;2.判别器&#xff0c;3.GAN网络训练&#xff0c;4.手写数字体生成。 …

Gitee 的公钥删不掉

公钥管理里已经没有公钥了&#xff0c; 仓库里还有&#xff0c;这是怎么回事&#xff1f; 这两个好像又没什么关系。 那为啥要搞两处呢&#xff1f; 个人信息里的公钥一直就没有仓库里使用的公钥&#xff0c; 删掉个人信息里的也没什么影响。 在仓库管理页面导入新公钥提示已…

【论文速读】|利用大语言模型实现现实世界代码的翻译:一项针对翻译到Rust语言的研究

本次分享论文&#xff1a;Towards Translating Real-World Code with LLMs: A Study of Translating to Rust 基本信息 原文作者&#xff1a;Hasan Ferit Eniser, Hanliang Zhang, Cristina David, Meng Wang, Maria Christakis, Brandon Paulsen, Joey Dodds, Daniel Kroeni…

【服务器07】之【GitHub项目管理】及【Unity异步加载场景】

登录GitHub官网 GitHub: Let’s build from here GitHub 注册账号 登录账号 输入一个自定义名字&#xff0c;点击创建存储库就可以了 现在我们下载Fork Fork - a fast and friendly git client for Mac and Windows (git-fork.com) 免费的 下载完成之后点击File下的Clone …

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑需求响应与储能寿命模型的火储协调优化运行策略》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

【研究】2012年后日股的“牛市起点”:安倍“出海潮”与巴菲特买点

2012 年-2017 年日股长周期反转定价的核心是房地产负面拖累结束&#xff0c;日本完成某种意义上的新旧动能宏观叙事转变。2012 年之后安倍经济学之于日股的长周期反转定价并非简单财政货币化的估值推动&#xff0c;而是鼓励企业出海获得营收利润增长。日股在2020年出现的巴菲特…

硬件基础知识——自学习梳理

计算机存储分为闪存和永久性存储。 硬盘&#xff08;永久存储&#xff09;主要分为机械磁盘和固态硬盘。 机械磁盘主要靠磁颗粒的正负极方向来存储0或1&#xff0c;且机械磁盘没有使用寿命。 固态硬盘就有使用寿命了&#xff0c;大概支持30w次的读写操作。 闪存使用的是电容…

Kotlin 中的解构

解构声明是 Kotlin 语言的一个特性&#xff0c;它允许我们从一个数据结构中提取多个变量&#xff0c;这样可以让我们的代码更加简洁易读&#xff0c;同时也提高了代码的可维护性。 在 Kotlin 中&#xff0c;解构可以用于多种数据类型&#xff0c;例如&#xff0c;列表&#xf…

数字化营销与传统营销的完美协奏曲!

在这个数字化的时代&#xff0c;营销的世界正在发生着巨大的变革&#xff01;数字化营销如火箭般崛起&#xff0c;但传统营销也并未过时。那么&#xff0c;如何让它们携手共进&#xff0c;创造出无与伦比的营销效果呢&#xff1f;今天&#xff0c;就让我们讲述一下蚓链数字化营…

Toco x Databend:Databend Cloud 如何满足 Web3 大数据服务的严苛考验?

Toco 是一家位于瑞士的 Web3 服务提供商&#xff0c;致力于通过彻底改变全球金融体系的陈旧观念来应对气候变化。他们提供了一种开创性的碳货币 Tocos&#xff0c;每个流通中的 Tocos 代表一吨二氧化碳当量&#xff0c;存储在安全可靠的数字钱包中。用户可以使用 Tocos 应用轻松…

捕捉过往的时光,5个步骤,安卓手机找回删除的照片

手机不仅仅是一个通讯工具&#xff0c;更是一个记录生活点滴的神器。手机照相机的出现&#xff0c;让我们随时随地都能捕捉到美好的瞬间&#xff0c;留下珍贵的回忆。然而&#xff0c;随着时间的推移&#xff0c;我们可能会不小心删除了这些照片&#xff0c;或者因为各种原因导…

JavaScript的运行机制

阐述几个概念 1. ECStack ( Execution Context Stack)执行环境栈 浏览器会在计算机内存中分配一块内存,专门用来供代码执行的 2. Heap堆内存 存放东西&#xff08;存放对象和方法即引用类型&#xff09; 3. EC ( Execution Context ) 执行上下文 代码自己执行所在的环境 …

论文翻译 | SELF-RAG: 学习通过自我反思来检索、生成和评估

Akari Asai, Zeqiu Wu, Yizhong Wang, Avirup Sil, Hannaneh Hajishirzi 华盛顿大学&#xff0c;IBM人工智能研究院 摘要 尽管大语言模型&#xff08;LLMs&#xff09;具有非凡的能力&#xff0c;但是它们经常产生不符合事实的响应&#xff0c;因为它们只依赖于它们封装的参数…

Java 应用的性能优化技巧和方法

Java应用性能优化是一个复杂且多层次的过程&#xff0c;涵盖了从代码编写到系统架构&#xff0c;再到运行环境配置的各个方面。 一、代码优化 1. 合理的数据结构和算法 选择合适的数据结构和算法对提高应用性能至关重要。Java提供了丰富的数据结构和算法库&#xff08;如Jav…

猫咪也怕油腻?选对猫粮是关键!福派斯鲜肉猫粮守护猫咪健康

亲爱的猫友们&#xff0c;我们都知道&#xff0c;猫咪的饮食健康是每一个铲屎官都非常关心的问题。最近&#xff0c;有些猫友向我反映&#xff0c;他们给猫主子喂食的猫粮油脂比较大&#xff0c;不禁让人担心这对猫咪是否真的好。 1️⃣ 首先&#xff0c;让我们来聊聊油脂在猫粮…

设计模式原则——里氏替换原则

设计模式原则 设计模式示例代码库地址&#xff1a; https://gitee.com/Jasonpupil/designPatterns 里氏替换原则 继承必须确保父类所拥有的性质在子类中依然成立 与开闭原则不同的是开闭原则可以改变父类原有的功能&#xff0c;里氏替换原则不能修改父类的原有的性质&#…

Clickhouse 的性能优化实践总结

文章目录 前言性能优化的原则数据结构优化内存优化磁盘优化网络优化CPU优化查询优化数据迁移优化 前言 ClickHouse是一个性能很强的OLAP数据库&#xff0c;性能强是建立在专业运维之上的&#xff0c;需要专业运维人员依据不同的业务需求对ClickHouse进行有针对性的优化。同一批…

用友 【U8+】供应链-暂估方式

【U8+】供应链-暂估方式 知识点:三种不同暂估方式的应用 应用场景: 对于采购业务如果货到票未到时,需要对采购入库进行估价入账,等收到发票时再按发票的成本核算采购入库成本。系统提供了三种暂估方式:月初回冲、单到回冲和单到补差。 操作流程: l 单到回冲 跨月全…