大厂Java面试题:MyBatis是如何进行分页的?分页插件的实现原理是什么?

大家好,我是王有志。

今天给大家带来的是一道来自京东的关于 MyBatis 实现分页功能的面试题:MyBatis是如何进行分页的?分页插件的实现原理是什么?
通常,分页的方式可以分为两种:

  • 逻辑(内存)分页
  • 物理分页

逻辑(内存)分页指的是数据库返回全部符合条件的数据,然后再通过程序代码对数据结果进行分页处理;物理分页指的是通过 SQL 语句查询,由数据库返回分页后的查询结果。
逻辑(内存)分页和物理分页各有优缺点,物理分页需要频繁的访问数据库,对数据库的负担较重,逻辑(内存)分页在数据量较大时也会对应用程序的性能造成较大的影响。

MyBatis 中实现逻辑(内存)分页

在 MyBatis 中实现逻辑(内存)分页,需要借助 MyBatis 提供的 RowBounds 对象。我们举个例子,首先定义 Mapper 接口:

List<UserDO>  logicalPagination(RowBounds rowBounds);

接着我们来写 MyBatis 映射器中的 SQL 语句:

<select id="logicalPagination" resultType="com.wyz.entity.UserDO">
select * from user
</select>

可以看到,虽然我们在 Java 的接口中定义了入参 RowBounds,但是在 MyBatis 映射器中并没有使用它。
最后我们来写单元测试代码:

public void testLogicalPagination() {Reader mysqlReader = Resources.getResourceAsReader("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mysqlReader);SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);RowBounds rowBounds = new RowBounds(0, 3);List<UserDO> users = userMapper.logicalPagination(rowBounds);log.info(JSON.toJSONString(users));sqlSession.close();
}

执行单元测试可以看到,虽然我们在 MyBatis 映射器中编写的 SQL 语句没有做任何限制,但实际上我们查询的结果只返回了 3 条数据,这就在 MyBatis 中实现逻辑(内存)分页的方式。

MyBatis 实现逻辑(内存)分页的原理

MyBatis 实现逻辑分页的原理并不复杂,简单来说,在执行查询语句前先创建 ResultSetHandler 对象,并持有 RowBounds 参数,在查询结果返回后,使用 ResultSetHandler 对象处理查询结果时,进行逻辑分页
首先是构建 ResultSetHandler 对象的流程,在执行查询前,MyBatis 会创建 ResultSetHandler 对象,整体调用流程如下:
在执行查询后,MyBatis 会调用 ResultSetHandler 对象进行结果集的处理,其中就包含对逻辑分析的处理,部分源码如下:

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();skipRows(resultSet, rowBounds);while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);Object rowValue = getRowValue(rsw, discriminatedResultMap, null);storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}
}

首先来看第 4 行中调用的DefaultResultSetHandler#skipRows方法:

private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {rs.absolute(rowBounds.getOffset());}} else {for (int i = 0; i < rowBounds.getOffset(); i++) {if (!rs.next()) {break;}}}
}

因为没有设置 ResultSet 的类型,因此我们不必关注 if 语句中的内容,直接来看 else 语句中的内容,逻辑非常清晰,根据传入的 RowBounds 对象的偏移量(即 offset)来移动 ResultSet 对象的游标位置,来保证逻辑(内存)分页时数据的起始位置。
接着来看第 5 行中调用的DefaultResultSetHandler#shouldProcessMoreRows方法:

private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}

该方法也并不复杂,是用来控制查询数据总量的,当 ResultContext 对象中的数据量小于 RowBounds 中最大数据量时,才会进入 while 循环,以此来保证查询到的数据不会超出我们指定的范围。
而 ResultContext 对象 resultCount 字段的变化,是在 while 循环中调用DefaultResultSetHandler#storeObject方法时改变的,这点就留给大家自行探索了。

MyBatis 中实现物理分页

在 MyBatis 中实现物理分页的常见方式有 3 种:

  • 使用数据库提供的功能,如 MySQL 中的 limit,Oracle 中的 rownum;
  • 通过自定义插件(拦截器)实现分页功能;
  • 使用 MyBatis 分页插件实现,如 PageHelper。

使用数据库的功能实现分页

我们以 MySQL 数据库为例展示一个完整的分页功能。
首先定义分页对象 Page,并定义 3 个字段,源码如下:

public class Page {/*** 当前页码*/private Integer currentPage;/*** 每页条数*/private Integer pageSize;/*** 总条数*/private Integer totalSize;
}

接着我们来写 Mapper 中的接口,此时要定义两个接口,第一个是通过 Page 对象查询数据的接口,第二个是查询全部数据数量的接口,源码如下:

List<UserDO> selectUsers(@Param("page")Page page);Long selectUsersCount();

最后我们来写 MyBatis 的映射器:

<select id="selectUsers" parameterType="com.wyz.entity.Page" resultType="com.wyz.entity.UserDO">select * from user<if test="page != null"><bind name="start" value="((page.currentPage) - 1) * page.pageSize"/>limit #{start}, #{page.pageSize}</if>
</select><select id="selectUsersCount" resultType="long">select count(*) from user
</select>

这里需要注意,我们与前端约定的页码是从 1 开始的,因此在 MyBatis 映射器中处理页码时需要减 1,不过即便如此,你也需要做好参数校验。另外,这里我们添加查询数据量的接口方法selectUsersCount是为了将数据量提供给前端,用于前端展示使用。

通过自定义插件(拦截器实现分页

上面的方式虽然能够实现分页的需求,但问题是如果每一个需要分页的查询都要添加这些内容的话,那么我们需要花费一些精力来维护这些 SQL 语句,那么有没有一劳永逸的方法?
还记得我们在 MyBatis核心配置讲解(下)中提到的 MyBatis 插件吗?MyBatis 中为每个关键场景都提供了插件的执行时机:

  • StatementHandler,SQL 语句处理器;
  • ParameterHandler,参数处理器;
  • Executor,MyBatis 执行器;
  • ResultSetHandler,结果集处理器。

如果想要实现物理分页,我们可以选择在 StatementHandler 和 Executor 阶段让插件介入,通过修改原始 SQL 来实现物理分页的功能。
首先我们来修改selectUsers方法对应的 MyBatis 映射器中的 SQL 语句,我们删除与分页相关的片段,源码如下:

<select id="selectUsers" resultType="com.wyz.entity.UserDO">select * from user
</select>

注意,这里我们没有删除接口的中 Page 参数,因为后面我们还要用到。
接着我们来定义自己的分页插件,这里我选择在StatementHandler#prepare的阶段,修改原始 SQL,使其具备分页的能力,源码如下:

@Intercepts({@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class, Integer.class})})
public class MyPageInterceptor implements Interceptor {@Override@SuppressWarnings("unchecked")public Object intercept(Invocation invocation) throws Throwable {StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = SystemMetaObject.forObject(statementHandler);// 获取参数ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");Map<String, Object> params = (Map<String, Object>) parameterHandler.getParameterObject();Page page = (Page)params.get("page");// 获取原始SQLBoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();// 修改原始SQLsql = sql + " limit " + page.getCurrentPage() + "," + page.getPageSize();metaObject.setValue("delegate.boundSql.sql", sql);return invocation.proceed();}// 省略部分方法
}

因为是在StatementHandler#prepare阶段让插件介入,此时 MyBatis 还没有生成 PreparedStatement 对象,此时我们只需要修改原始 SQL 语句即可。
接着我们在 mybatis-config.xml 配置我们自定义的插件:

<configuration><plugins><plugin interceptor="com.wyz.customize.plugin.MyPageInterceptor"/></plugins>
</configuration>

最后执行单元测试,可以看到结果如我们预期的那样,实现了分页功能。

上面的自定义分页插件只实现了修改原始查询 SQL 语句的能力,依旧需要我们自行实现查询总数的接口,不过,我们也可以在插件中自动生成查询总数的方法。
Tips:上面的自定义插件只是为了展示,功能很不完善,健壮性也很差,不能在生产环境中使用。

使用分页插件来实现分页

MyBatis 中最常用的分页插件就是 PageHelper 了,它的用法非常简单。
首先是引入 PageHelper 插件:

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

接着在 mybatis-config.xml 中配置 PageHelper 的插件:

<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>

最后,我们来使用 PageHelper 来写一个单元测试:

public void testPageHelper() {PageHelper.startPage(0, 3);List<UserDO> users = userMapper.selectAll();PageInfo<UserDO> pageInfo = new PageInfo<>(users);long count = pageInfo.getTotal();sqlSession.close();
}

与我们自定义的分页插件不同的是,PageHelper 并不需要我们传入分页参数,而是通过PageHelper#startPage设置分页相关参数即可。PageHelper 是通过 ThreadLocal 变量来保证同一个线程中的 PageInterceptor 能够获取到分页参数的。
核心原理上,PageHelper 与我们自定义实现的分页插件并没有太大差别,都是通过为 SQL 语句添加“limit”来实现的分页功能,只不过 PageHelper 选择的处理阶段为Executor#query,PageInterceptor 类的声明如下:

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),}
)
public class PageInterceptor implements Interceptor

因为 PageHelper 的整体逻辑并不复杂,核心原理也与我们之前自定义实现的分页插件相同,所以 PageHelper 的源码就留给大家自行分析了。
Tips:当然了, PageHelper 的功能更加完成,代码健壮性更好。


好了,今天的内容就到这里了,如果本文对你有帮助的话,希望多多点赞支持,如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核 Java 技术的金融摸鱼侠王有志,我们下次再见!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智慧园区综合物业管理平台解决方案PPT(130页精品)

我们对智慧园区的理解 智慧园区&#xff0c;是通过信息技术和各类资源的整合&#xff0c;充分降低企业运营成本&#xff0c;提高工作效率&#xff0c;加强各类园区创新、服务和管理能力&#xff0c;为园区铸就一套超强的软实力。智慧园区的实现是多技术融合、多系统融合、多领域…

苹果挖走大量谷歌人才,建立神秘人工智能实验室;李飞飞创业成立「空间智能」公司丨 RTE 开发者日报 Vol.197

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

从简单逻辑到复杂计算:感知机的进化与其在现代深度学习和人工智能中的应用(上)

文章目录 引言第一章&#xff1a;感知机是什么第二章&#xff1a;简单逻辑电路第三章&#xff1a;感知机的实现3.1 简单的与门实现3.2 导入权重和偏置3.3 使用权重和偏置的实现实现与门实现与非门和或门 文章文上下两节 从简单逻辑到复杂计算&#xff1a;感知机的进化与其在现代…

ESG视角下的多期DID构建(2009-2022年)4.5万+数据

随着ESG信息越来越受到重视&#xff0c;一些第三方评级机构开始推出ESG评级产品&#xff0c;目前在第三方数据库能够查到华证、富时罗素、商道融绿、社会价值投资联盟以及Wind自有的ESG评级数据等。其中&#xff0c;商道融绿是中国最早发布ESG评级数据的机构&#xff0c;也是国…

【Linux 基础 IO】文件系统

文章目录 1.初步理解文件2. fopen ( )的详解 1.初步理解文件 &#x1f427;① 打开文件&#xff1a; 本质是进程打开文件&#xff1b; &#x1f427;②文件没有被打开的时候在哪里呢&#xff1f; ----- 在磁盘中&#xff1b; &#x1f427;③进程可以打开很多个文件吗&#xff…

Vue入门到关门之Vue3项目创建

一、vue3介绍 1、为什么要学习vue3&#xff1f; vue3的变化&#xff1a; 首先vue3完全兼容vue2&#xff0c;但是vue3不建议用vue2的写法&#xff1b;其次&#xff0c;vue3拥抱TypeScript&#xff0c;之前vue2使用的JavaScript&#xff0c;ts完全兼容js 最后之前学的vue2 是…

【Linux】操作系统

上一篇博客我们从硬件的角度谈了计算机&#xff0c;我们说到了计算机的效率跟操作系统写的好不好有着直接的关系&#xff0c;那么这篇博客我们从软件的角度&#xff0c;就来谈一谈究竟什么是操作系统&#xff0c;为什么要有操作系统&#xff1f; 首先我们来大体的认识一下操作…

智慧工地)智慧工地标准化方案(107页)

2.2 设计思路 对于某某智慧工地管理系统的建设&#xff0c;绝不是对各个子系统进行简单堆砌&#xff0c;而是在满足各子系统功能的基础上&#xff0c;寻求内部各子系统之间、与外部其它智能化系统之间的完美结合。系统主要依托于智慧工地管理平台&#xff0c;来实现对众多子系统…

27-代码随想录三数之和

15. 三数之和 中等 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重…

Java基础教程 - 4 流程控制

更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 4 流程控制 4.1 分支结构…

从零开始学RSA: [WUSTCTF2020]情书等5题

1 [WUSTCTF2020]情书 题目 Premise: Enumerate the alphabet by 0、1、2、..... 、25 Using the RSA system Encryption:0156 0821 1616 0041 0140 2130 1616 0793 Public Key:2537 and 13 Private Key:2537 and 937flag: wctf2020{Decryption}解题 前提&#xff1a;用0、…

Labels and Databases for Mac:强大的标签与数据库管理工具

Labels and Databases for Mac是一款集标签制作与数据库管理于一体的强大工具&#xff0c;专为Mac用户打造&#xff0c;旨在提供高效、便捷的标签制作与数据管理体验。 这款软件拥有丰富的内置标签格式&#xff0c;用户可轻松创建各种标签、信封和卡片&#xff0c;满足个性化需…