MyBatis 分页插件 PageHelper

2023-09-21 12:09:17

前言

分页插件PageHelper是我们使用Mybatis到的比较多的插件应用,适用于任何复杂的单表、多表分页查询操作。本文介绍PageHelper的使用及原理。

PageHelper 应用

添加依赖

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

在全局配置文件中注册

<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
    <property name="dialect" value="mysql" />
    <!-- 该参数默认为false -->
    <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
    <!-- 和startPage中的pageNum效果一样 -->
    <property name="offsetAsPageNum" value="true" />
    <!-- 该参数默认为false -->
    <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
    <property name="rowBoundsWithCount" value="true" />
    <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
    <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) -->
    <property name="pageSizeZero" value="true" />
    <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
    <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
    <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
    <property name="reasonable" value="false" />
    <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
    <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
    <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->
    <!-- 不理解该含义的前提下,不要随便复制该配置 -->
    <property name="params" value="pageNum=start;pageSize=limit;" />
    <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
    <property name="returnPageInfo" value="check" />
</plugin>

在执行查询操作之前设置了一句PageHelper.startPage(1,5); 即可实现分页效果。

实现原理剖析

PageHelper也必然要实现 Interceptor ,通过源码我们可以发现是PageHelper方法头部添加的注解,声明了该拦截器拦截的是Executor的query方法。

image.png

执行查询操作的时候, Executor.query() 方法的执行本质上是执行 Executor的代理对象的方法。先来看下Plugin中的invoke方法

  /**
   * 代理对象方法被调用时执行的代码
   * @param proxy
   * @param method
   * @param args
   * @return
   * @throws Throwable
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 获取当前方法所在类或接口中,可被当前Interceptor拦截的方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        // 当前调用的方法需要被拦截 执行拦截操作
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 不需要拦截 则调用 目标对象中的方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

interceptor.intercept(new Invocation(target, method, args));方法的执行会进入到 PageHelper的intercept方法中

    /**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }

intercept方法

    /**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) { // 多数据源
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else { // 单数据源
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }

在interceptor方法中首先会获取一个SqlUtils对象

SqlUtil:数据库类型专用sql工具类,一个数据库url对应一个SqlUtil实例,SqlUtil内有一个Parser对象,如果是mysql,它是MysqlParser,如果是oracle,它是OracleParser,这个Parser对象是SqlUtil不同实例的主要存在价值。执行count查询、设置Parser对象、执行分页查询、保存Page分页对象等功能,均由SqlUtil来完成。

initSqlUtil 方法创建的解析器

public static Parser newParser(Dialect dialect) {
        Parser parser = null;
        switch (dialect) {
            case mysql:
            case mariadb:
            case sqlite:
                parser = new MysqlParser();
                break;
            case oracle:
                parser = new OracleParser();
                break;
            case hsqldb:
                parser = new HsqldbParser();
                break;
            case sqlserver:
                parser = new SqlServerParser();
                break;
            case sqlserver2012:
                parser = new SqlServer2012Dialect();
                break;
            case db2:
                parser = new Db2Parser();
                break;
            case postgresql:
                parser = new PostgreSQLParser();
                break;
            case informix:
                parser = new InformixParser();
                break;
            case h2:
                parser = new H2Parser();
                break;
            default:
                throw new RuntimeException("分页插件" + dialect + "方言错误!");
        }
        return parser;
    }

不同的数据库方言,创建了对应的解析器。

sqlUtil.processPage(invocation);方法中会完成分页SQL的绑定

image.png

进入 PageSqlSource 的getBoundSql方法中

image.png

    /**
     * 获取BoundSql
     *
     * @param parameterObject
     * @return
     */
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        Boolean count = getCount();
        if (count == null) {
            return getDefaultBoundSql(parameterObject);
        } else if (count) {
            return getCountBoundSql(parameterObject);
        } else {
            return getPageBoundSql(parameterObject);
        }
    }

getPageBoundSql获取分页的SQL语句,在这个方法中可以发现查询总的记录数的SQL生成

    @Override
    protected BoundSql getPageBoundSql(Object parameterObject) {
        String tempSql = sql;
        String orderBy = PageHelper.getOrderBy();
        if (orderBy != null) {
            tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        tempSql = localParser.get().getPageSql(tempSql);
        return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
    }

最终在这个方法中生成了对应数据库的分页语句

image.png

应用场景分析

作用描述实现方式
水平分表一张费用表按月度拆分为12张表。fee_202001-202012。当查询条件出现月度(tran_month)时,把select语句中的逻辑表名修改为对应的月份表。对query update方法进行拦截在接口上添加注解,通过反射获取接口注解,根据注解上配置的参数进行分表,修改原SQL,例如id取模,按月分表
数据脱敏手机号和身份证在数据库完整存储。但是返回给用户,屏蔽手机号的中间四位。屏蔽身份证号中的出生日期。query——对结果集脱敏
菜单权限控制不同的用户登录,查询菜单权限表时获得不同的结果,在前端展示不同的菜单对query方法进行拦截在方法上添加注解,根据权限配置,以及用户登录信息,在SQL上加上权限过滤条件
黑白名单有些SQL语句在生产环境中是不允许执行的,比如like %%对Executor的update和query方法进行拦截,将拦截的SQL语句和黑白名单进行比较,控制SQL语句的执行
全局唯一ID在高并发的环境下传统的生成ID的方式不太适用,这时我们就需要考虑其他方式了创建插件拦截Executor的insert方法,通过UUID或者雪花算法来生成ID,并修改SQL中的插入信息
更多推荐

Win10 家庭版 - 解决应用程序无法启动,因为应用程序的并行配置不正确的问题(System Default Context”的激活上下文生成失败)

Win10家庭版-解决应用程序无法启动,因为应用程序的并行配置不正确的问题(SystemDefaultContext”的激活上下文生成失败)系统环境遇到问题试过过程解决办法前天的时候,女盆友公司电脑遇到个问题:几乎所有的exe程序和软件都不能启动或者运行。我的第一个解决办法:重装即可。结果人家嫌挨个重装太麻烦。于是乎,

收款码的费率都是多少

不管是微信还是支付宝,商户最低的收款手续费率可以达到0.2%费率。一般我们普通商户的收款费率一般在0.6左右,当然也有使用0.3的,也就是1万元的费率是30-60块钱,对于一些流水比较大的商家来说,确实很有必要把这个手续费率降低。什么是收款手续费率?因为不管是微信还是支付宝,都是盈利性质的公司,他们开发了收款码这个功能

归并排序的思想

归并排序是一种基于分治思想的经典排序算法。它将待排序的数组分成两个部分,然后递归地对这两个部分进行排序,最后再将排序好的两个部分归并成一个有序的数组。具体实现过程如下:1.将待排序数组不断二分,直到只剩下一个元素,此时该元素就是有序的。2.将相邻的两个有序数组合并成一个有序数组。合并时,对于两个数组中首位元素进行比较,

SpringBoot中Filter和Interceptor快速入门

一、Filter1.定义说明:filter文件里面DemoFilter类实现Filter接口。packagecom.itheima.filter;importjavax.servlet.*;importjavax.servlet.annotation.WebFilter;importjava.io.IOExceptio

计算机二级python基础题刷题笔记(三)

hello,看到三的小伙伴们你们已经超过30%的对手啦!接下来也要加油呀代码没有最好,只有更好,如果你有更好的想法答案欢迎在评论区里发表呀1、将程序里定义好的std列表里的姓名和成绩与已经定义好的模板拼成一段话,显示在屏幕里。std=[['张三',90,87,95],['李四',83,80,87],['王五',73,5

临沂ITSS认证流程,认证条件

ITSS认证流程,认证条件一、ITSS的意义ITSS认证——信息技术服务标准,是在工业和信息化部、国家标准化委的领导和支持下,由ITSS工作组研制的一套IT服务领域的标准库和一套提供IT服务的方法论。ITSS认证-信息技术服务标准是一套成体系和综合配套的信息技术服务标准库,全面规范了IT服务产品及其组成要素,用于指导实

进阶JS-reduce用法

reduce()reduce()方法为循环遍历数组,将其结果汇总为单个返回值,最常用的场景有数组求和、数组求积、数组中元素出现的次数、数组去重等等。语法:arr.reduce(function(prev,cur,index,arr){...},init);参数:prev必需。累计器累计回调的返回值;表示上一次调用回调时

攻防世界-web-ics-05

ics-05GFSJ0332积分3金币377最佳Writeup由darkless提供收藏反馈难度:3方向:Web题解数:35解出人数:6403题目来源:XCTF题目描述:其他破坏者会利用工控云管理系统设备维护中心的后门入侵系统题目场景:http://61.147.171.105:53619100%倒计时:3时59分0秒

代码随想录训练营第四十八天|198.打家劫舍 ● 213.打家劫舍II ● 337.打家劫舍III

198.打家劫舍力扣题目链接(opensnewwindow)你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一

ChatGPT:Java中的try-catch-finally及return语句的执行顺序解析

ChatGPT:Java中的try-catch-finally及return语句的执行顺序解析Java中的try-catch-finally,为什么try里面有return语句,finally里面的语句还能执行ChatGPT:在Java中,当在try块中存在return语句时,finally块中的语句仍然会执行。这是因

深入理解Ansible:简化服务器管理和自动化任务的利器

导言:在现代IT环境中,管理和维护服务器和应用程序变得愈发复杂,而自动化工具如Ansible成为了解决这些挑战的关键。本文将详细介绍什么是Ansible,以及如何使用它来简化服务器管理和自动化任务。第一部分:什么是Ansible?Ansible是一款开源的自动化工具,用于配置管理、应用程序部署和任务自动化。它基于SSH

热文推荐