大厂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");

    // 获取原始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 技术的金融摸鱼侠王有志,我们下次再见!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PON网络和HFC网络

目录 1.概念 2.分类 3.重点 1.概念 PON PON是一种典型的无源光纤网络&#xff0c;是一种点到多点的无源光纤接入技术。 是指 (光配线网中) 不含有任何电子器件及电子电源&#xff0c;ODN全部由光分路器 (Splitter) 等无源器件组成&#xff0c;不需要贵重的有源电子设备。一个…

Java | Leetcode Java题解之第73题矩阵置零

题目&#xff1a; 题解&#xff1a; class Solution {public void setZeroes(int[][] matrix) {int m matrix.length, n matrix[0].length;boolean flagCol0 false;for (int i 0; i < m; i) {if (matrix[i][0] 0) {flagCol0 true;}for (int j 1; j < n; j) {if (…

【1小时掌握速通深度学习面试8】生成模型-中

目录 28.DBN与DBM 有什么区别? 29.VAE如何控制生成图像的类别? 30.如何修改VAE的损失函数&#xff0c;使得隐藏层的编码是相互解耦的? 31.自回归方法如何应用在生成模型上? 32.原始 VAE存在哪些问题? 有哪些改进方式? 33.如何将VAE与GAN 进行结合&#xff1f; 34.…

【LeetCode】环形队列实现

目录 前言1. 环形队列概念2. 循环队列实现设计3. 功能实现3.1 定义3.2 初始化3.3 判断队列是否为空3.4 判断队列是否为满3.5 入栈3.6 出栈3.7 获取队头数据3.8 获取队尾数据3.9 销毁 4. 总结5. 完整通过代码 前言 之前我们学习环形链表相关问题&#xff0c;现在我们来看看环形…

抖音爆火的QQ价格评估前端源码

最近抖音很火直播给别人测qq价值多少&#xff0c;这个源码只有前端&#xff0c; 包含激活码验证页&#xff0c;评估页 源码免费下载地址抄笔记 (chaobiji.cn)

流畅的python-学习笔记_符合python风格的对象

对象表示形式 查看对象说明&#xff0c;可以通过__repr__和__str__方法&#xff0c;前者主要用于开发者&#xff0c;后者主要用于用户&#xff0c;这两个方法分别对内置函数repr和str函数提供支持 向量类 备选构造方法 classmethod和staticmethod staticmethod用的不是特别…

力扣每日一练(螺旋矩阵)

54. 螺旋矩阵 - 力扣&#xff08;LeetCode&#xff09; 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,…

数据库提权

1.此时实验需要用到的软件&#xff1a; &#xff08;1&#xff09;phpStudy该程序包集成最新的ApachePHPMySQL phpMyAdminZendOptimizer,一次性安装,无须配置即可使用,是非常方便、好用的PHP调试环境.该程序不仅包括PHP调试环境,还包括了开发工具、开发手册等.总之学习PHP只需…

STM32——TIMER(定时器)篇

技术笔记&#xff01; 1. 定时器概述&#xff08;了解&#xff09; 1.1 软件定时器原理 使用纯软件&#xff08;CPU死等&#xff09;的方式实现定时&#xff08;延时&#xff09;功能 缺点&#xff1a;1. 延时不准确 2. CPU死等。 1.2 定时器定时原理 1.…

在Codelab对llama3做Lora Fine tune微调

Unsloth 高效微调大模型的工具&#xff0c;通过Unsloth微调Llama3, Mistral, Gemma 速度提升2-5倍&#xff0c;内存减少70%&#xff01; Codelab 创建一个jupyter notebook 选择 T4 GPU 安装Fine tune 相关的lib %%capture import torch major_version, minor_version torch…

等保测评—Linux-CentOS标准范例截图

密码输入错误无法登录 用户账户情况包含root、guanli、shenji 查看审计用户权限 身份鉴别&#xff1a; cat /etc/passwd&#xff0c;核查用户名和 UID&#xff0c;是否存在同样的用户名和 UID cat /etc/shadow&#xff0c;查看文件中各用户名状态 &#xff0c; 核查密码一栏为…

day1Qt作业

#include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {this->resize(540,415);//窗口大小this->setFixedSize(540,415);//固定窗口大小this->setWindowTitle("QQ");//标题this->setWindowIcon(QIcon("E:\\hqyjap…

获取转转数据,研究完转转请求,tx在算法方面很友好。

本篇文章仅供学习讨论。 文章中涉及到的代码、实例&#xff0c;仅是个人日常学习研究的部分成果。 如有不当&#xff0c;请联系删除。 在研究完阿里的算法以后&#xff08;其实很难说研究完&#xff0c;还有很多内容没有研究透&#xff0c;只能说暂时告一段落&#xff09;&…

【CTF Web】XCTF GFSJ0475 get_post Writeup(HTTP协议+GET请求+POST请求)

get_post X老师告诉小宁同学HTTP通常使用两种请求方法&#xff0c;你知道是哪两种吗&#xff1f; 解法 用 Postman 发送一个 GET 请求&#xff0c;提交一个名为a,值为1的变量。 http://61.147.171.105:65402/?a1用 Postman 发送一个 POST 请求&#xff0c;提交一个名为b,值为…

Embeddings原理、使用方法、优缺点、案例以及注意事项

Embeddings是一种将高维数据映射到低维空间的技术&#xff0c;常用于处理自然语言处理&#xff08;NLP&#xff09;和计算机视觉&#xff08;CV&#xff09;任务。Embeddings可以将复杂的高维数据转换为低维稠密向量&#xff0c;使得数据可以更容易地进行处理和分析。本文将介绍…

国内首发 | CSA大中华区启动《AI安全产业图谱(2024)》调研

在人工智能&#xff08;AI&#xff09;技术的快速发展浪潮中&#xff0c;AI安全已成为全球关注的焦点。为应对AI安全带来的挑战&#xff0c;确保AI技术的健康发展&#xff0c;全球范围内的研究机构、企业和技术社区都在积极探索解决方案。 在这一背景下&#xff0c;CSA大中华区…

在2G到4g小区重选过程中,4g频点没有优先级信息,最后UE无法重选到4g,是否正常?

这个确实是老问题了&#xff0c;要翻开GSM 的协议找答案。 GSM cell reselection算法分为cell ranking based和priority based两种方式。cell ranking based 只能从GSM重选到UTRAN&#xff1b;而priority based则可以重选到UTRAN和EUTRA。 根据priority based重选算法的描述&am…

【WP】第一届 “帕鲁杯“ - CTF挑战赛 Web 全解

Web Web-签到 考点&#xff1a;审计py代码 from flask import Flask, request, jsonify import requests from flag import flag # 假设从 flag.py 文件中导入了 flag 函数 app Flask(__name__)app.route(/, methods[GET, POST]) def getinfo():url request.args.get(url)i…

java08基础(值传递和引用传递 类和对象)

目录 一. 值传递和引用传递 1. 值传递 2. 引用传递 二. 面向对象思想 三. 类和对象 1. 类 2. 对象 2.1 使用 2.2 成员变量和局部变量区别 2.3 操作成员方法 2.4 this关键字(初始) 2.5 构造方法 (见java09) 一. 值传递和引用传递 1. 值传递 值传递是指在调用函数时将…

webpack4和webpack5区别1---loader

webpack4处理图片和字体的loader file-loader file-loader的作用是处理webpack中的静态资源文件。File Loader可以将各种类型的文件&#xff0c;如图像、字体、视频等转换为模块并加载到Web应用程序中。它通过import或require语句引入文件资源&#xff0c;并将其放置在输出目…