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

news2024/11/24 16:12:21

大家好,我是王有志。

今天给大家带来的是一道来自京东的关于 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");

    // 获取原始SQL
    BoundSql boundSql = statementHandler.getBoundSql();
    String sql = boundSql.getSql();

    // 修改原始SQL
    sql = 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.coloradmin.cn/o/1646535.html

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

相关文章

c#绘制渐变色的Led

项目场景&#xff1a; c#绘制渐变色的button using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; using static System.Windows.Forms.AxHost;namespace WindowsFormsApp2 {public class Gradie…

基于FPGA的数字信号处理(9)--定点数据的两种溢出处理模式:饱和(Saturate)和绕回(Wrap)

1、前言 在逻辑设计中&#xff0c;为了保证运算结果的正确性&#xff0c;常常需要对结果的位宽进行扩展。比如2个3bits的无符号数相加&#xff0c;只有将结果设定为4bits&#xff0c;才能保证结果一定是正确的。不然&#xff0c;某些情况如77 14(1110)&#xff0c;如果结果只…

FileBird Pro插件下载:革新您的WordPress媒体库管理

WordPress媒体库是您网站的重要组成部分&#xff0c;它存储了所有的图片、视频、文档等文件。但随着网站的扩展&#xff0c;媒体库的管理变得越来越复杂。FileBird Pro插件&#xff0c;作为一款专为WordPress用户设计的媒体库管理工具&#xff0c;以其直观的界面和强大的功能&a…

嵌入式系统应用-拓展-FLASH之操作 SFUD (Serial Flash Universal Driver)之KEIL移植

1 SFUD介绍 1.1 初步介绍 SFUD 是一个开源的串行 SPI 闪存通用驱动库。由于市面上有各种类型的串行闪存设备&#xff0c;每种设备都具有不同的规格和指令&#xff0c;因此 SFUD 的设计目的是解决这些差异。这使得我们的产品可以支持不同品牌和规格的闪存&#xff0c;增强了软…

幻兽帕鲁游戏主机多少钱?幻兽帕鲁游戏服务器一个月仅需32元

随着游戏产业的蓬勃发展&#xff0c;腾讯云紧跟潮流&#xff0c;推出了针对热门游戏《幻兽帕鲁Palworld》的专属游戏服务器。对于广大游戏爱好者来说&#xff0c;这无疑是一个激动人心的消息。那么&#xff0c;腾讯云幻兽帕鲁游戏主机到底多少钱呢&#xff1f;让我们一起来揭晓…

施耐德EOCR3BZ2-WRAUH数显型马达保护器

施耐德EOCR3BZ2-WRAUH数显型马达保护器 ◆ 高性能多功能电动机保护器 ◆ 实时监测/高精度 ◆ 可为单相或三相交流负载提供可靠保护 ◆ 保护功能&#xff1a;过电流、欠电流、反转、缺相、失速、电流不平衡和接地保护功能 ◆ 附加功能&#xff1a;掉电保护、运行时间累计、…

太阳能4G/5G无线视频监控系统解决方案(下)

目录 一、系统简介 &#xff08;一&#xff09;系统概述 &#xff08;二&#xff09;系统特点 &#xff08;三&#xff09;应用场景 二、系统原理和架构 &#xff08;一&#xff09;系统原理 1、拓扑图 2、系统组成介绍 &#xff08;二&#xff09;太阳能供电子系统 …

计算机SCI期刊,中科院2区,IF=6+,自引率低,专业认可度高!

一、期刊名称 Journal of King Saud University-Computer and Information Sciences 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;计算机科学 影响因子&#xff1a;6.9 中科院分区&#xff1a;2区 出版方式&#xff1a;开放出版 版面费&#xff1a;$…

ADS基础教程9-理想模型和厂商模型实现及对比

目录 一、概要二、厂商库使用1.新建cell2.调用厂商库中元器件3.元器件替换及参数选择4.完成参数选择5.导入子图 三、仿真实现注意事项 一、概要 本文将介绍在ADS中调用厂商提供的库&#xff0c;来进行原理图仿真&#xff0c;并实现与ADS系统提供的理想元器件之间的比较。 二、…

Linux服务器常用巡检命令

在Linux服务器上进行常规巡检是确保服务器稳定性和安全性的重要措施之一。以下是一些常用的巡检命令和技巧&#xff1a; 1. 查看系统信息 1.1 系统信息显示 命令&#xff1a;uname -a ​​​​ [rootlinux100 ~]# uname -a Linux linux100 4.15.0-70-generic #79-Ubuntu SMP…

Java特性之设计模式【享元模式】

一、享元模式 概述 享元模式&#xff08;Flyweight Pattern&#xff09;主要用于减少创建对象的数量&#xff0c;以减少内存占用和提高性能。这种类型的设计模式属于结构型模式&#xff0c;它提供了减少对象数量从而改善应用所需的对象结构的方式 享元模式尝试重用现有的同类对…

腾讯地图商业授权说明一篇文章讲清楚如何操作

最近在使用腾讯地图&#xff0c;发现我要上架应用商店APP需要我有地图的授权书。 认真研究了一下原来腾讯地图现在要收费了&#xff0c;如果你打算以商业目的使用它&#xff0c;比如对第三方用户收费或者进行项目投标等&#xff0c;就需要先获取腾讯位置服务的商业授权许可。申…

Linux网络部分——DNS域名解析服务

目录 1. 域名结构 2. 系统根据域名查找IP地址的过程 3.DNS域名解析方式 4.DNS域名解析的工作原理【☆】 5.域名解析查询方式 6.搭建主从DNS域名服务器 ①初始化操作主服务器和从服务器&#xff0c;安装BIND软件 ②修改主服务器的主配置文件、区域配置文件、区域数…

(数据分析方法)长期趋势分析

目录 一、定义 二、目的 三、方法 1、移动平均法 (1)、简单移动平均法 (2)、加权移动平均法 (3)、指数平滑法 2、最小二乘法 3、线性回归 1、数据预处理 2、观察数据分布建立假设模型 3、定义损失函数 4、批量梯度下降 5、优化 4、LSTM 时序分析 5、特征工程 一…

线程详解(接上篇博客)

目录 1.生产者消费者模型; 2.基于环形队列的生产者消费者模型; 3.线程池; 4.STL, 智能指针, 线程安全; 5.读者写者问题. 前言: 本篇博客博主五一假期都在肝的一篇, 希望xdm点点三连, 博主感谢了 onz !!! 1.生产者消费者模型 321原则:(便于记忆) 3是指3种关系: 生产者和生产…

Chat2DB Pro 重磅发布 !!!

在过去的几个月中&#xff0c;我们的团队一直默默耕耘&#xff0c;全心投入到Chat2DB Pro版本的开发之中。这段时间里&#xff0c;我们暂停了新动态的发布&#xff0c;以至于有趣的误解在社群中出现&#xff0c;有人调侃我们是否“倒闭”了。然而&#xff0c;我们今天携带着全新…

Java-(乘法表之后)增强for循环

这里我们先做个了解&#xff0c;之后我会在数组中进行详细介绍Java5引入了一种主要用于数组或集合的增强型for循环Java增强型for循环语法格式如下 For(声明语句&#xff1a;表达式&#xff09;{ //代码语句 } 声明语句&#xff1a;声明新的局部变量&#xff0c;该变量的类型…

【智能算法】PID搜索算法(PSA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献5.代码获取 1.背景 2023年&#xff0c;Y Gao受到PID控制理论启发&#xff0c;提出了PID搜索算法&#xff08;PID-based Search Algorithm, PSA&#xff09;。 2.算法原理 2.1算法思想 PID算法是控制领域的…

CTF(Web)中关于执行读取文件命令的相关知识与绕过技巧

在我遇到的题目中&#xff0c;想要读取文件必然是要执行cat /flag这个命令&#xff0c;但是题目当然不会这么轻松。让你直接cat出来&#xff0c;必然会有各种各样的滤过条件&#xff0c;你要做的就是尝试各种方法在cat /flag的基础上进行各种操作构建出最终的payload。 下面我…

Redis学习汇总

目录 1.Linux环境下安装redis 2.redis的数据结构及命令 3.redis.conf配置文件常用配置 3.redis的事务操作 4.redis实现乐观锁 5.通过jedis操作redis 6.Springboot集成redis 7.自定义一个RedisTemplate 8.持久化策略 RDB和AOF 9.redis集群环境搭建 10.哨兵模式 11.缓…