MyBatis分页

MyBatis分页三种方式

1.limit 分页

语法

limit ${startPos},${pageSize}
  • startPos 代表从第startPos条数据开始 (起始行数,从0开始)
  • pageSize 代表查pageSize条数据(数据量)

limit 0,10; 代表从第0条数据开始 查10条数据, 第二页的时候就是 limit 10,10;

eg1: getByLimit

eg:2:查询第k名

查询最大、最小 ------------------ 使用聚合函数

查询第k名(升序、逆序)------ 使用order by 排序 + limit 进行挑选。

查询某个字段第k名的数据。注意是k-1(从0开始!)

select * from 表名 order by 字段 desc/asc limit k-1, 1

中小数据量时,可以使用联合索引(where限制条件和orderby排序的条件组合)提升查询速度。

数据量增加,limit语句的偏移量会越大,速度会慢,可以通过子查询提升分页效率


select * 
from attend_lesson_record 
where id >= (select id from attend_lesson_record limit 10, 1) limit 10 

2.RowBounds分页(不推荐使用)

省略了limit的内容, 属于逻辑分页,即实际上sql查询的是所有的数据,在业务层进行了分页而已,比较占用内存,而且数据更新不及时,可能会有一定的滞后性!不推荐使用!

RowBounds对象有2个属性,offset和limit。

  • offset:起始行数
  • limit:需要的数据行数

因此,取出来的数据就是:从第offset+1行开始,取limit行

分页原理(拦截器):

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap()方法源码。

Mybatis中使用RowBounds实现分页的大体思路:
先取出所有数据,然后游标移动到offset位置,循环取limit条数据,然后把剩下的数据舍弃。

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
  DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
//跳过RowBounds设置的offset值
   skipRows(rsw.getResultSet(), rowBounds);
//判断数据是否小于limit,如果小于limit的话就不断的循环取值
   while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
     ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
     Object rowValue = getRowValue(rsw, discriminatedResultMap);
     storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
   }
}

private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
	//判断数据是否小于limit,小于返回true
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}

  //跳过不需要的行,应该就是rowbounds设置的limit和offset
  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 {
	  //跳过RowBounds中设置的offset条数据,只能逐条滚动到指定位置
      for (int i = 0; i < rowBounds.getOffset(); i++) {
        rs.next();
      }
    }
}

eg:getByRowBounds

xml的sql查询全部,调用sql的方法传入 rowbounds

RowBounds rowbounds = new RowBounds(offset, Integer.parseInt(pageSize));

问题:select * from user where id>0 limit 0,10
RowBounds会将id>0的所有数据全都加载到内存中,然后截取前10行,若id>0有100万条,则100万条数据都会加载到内存中,从而造成内存OOM。

3.Mybatis_PageHelper分页插件

依赖

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

核心配置

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

eg

 @Override
    public List<AttendLessonRecord> getByPageHelper(Integer pageNum, Integer pageSize) {

        /**
         * 几种写法
         */
//        //doSelectPage 创建接口
//        Page<AttendLessonRecord> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
//            @Override
//            public void doSelect() {
//                attendLessonRecordMapper.getAllRecord();
//            }
//        });
//        //doSelectPage lambda写法
//        PageHelper.startPage(pageNum, pageSize).doSelectPage(()-> attendLessonRecordMapper.getAllRecord());
//
//        //doSelectPageInfo 创建接口
//        PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
//            @Override
//            public void doSelect() {
//                attendLessonRecordMapper.getAllRecord();
//            }
//        });
//        //doSelectPageInfo jdk8 lambda 表达式
//        PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(()-> attendLessonRecordMapper.getAllRecord());

        PageHelper.startPage(pageNum, pageSize);

        return attendLessonRecordMapper.getAllRecord();
    }
拦截器

执行顺序

拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行,用于对处理器进行预处理和后处理。在Spring MVC 与Spring Boot 中使用拦截器一般是实现HandlerInterceptor 接口。

作用:将多个控制器中共有代码放入拦截器可以减少控制器代码冗余,可以构成拦截器栈,完成特定功能。比如日志记录、登录判断、权限检查等作用。

执行流程

(1)、程序先执行preHandle()方法,如果该方法的返回值为true,则程序会继续向下执行处理器中的方法,否则将不再向下执行;

(2)、在业务处理器(即控制器Controller类)处理完请求后,会执行postHandle()方法,然后会通过DispatcherServlet向客户端返回响应;

(3)、在DispatcherServlet处理完请求后,才会执行afterCompletion()方法。

HandlerInterceptor

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface HandlerInterceptor {
    //这个方法可以实现处理器的预处理,也就是它会在handler 方法执行之前就开始执行。当返回值是true 时表示继续执行,返回false 时则不会执行后续的拦截器或处理器。作用:身份验证,身份授权等。
    boolean preHandle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;

    //这个方法是后处理回调方法,也就是在控制器完成后(试图渲染之前)执行。作用:将公用的模型数据传到视图,也可以在这里统一指定视图(菜单导航等)。
    void postHandle(HttpServletRequest var1, HttpServletResponse var2, Object var3, ModelAndView var4) throws Exception;

    //这个方法是请求处理完毕后的回调方法,即在视图渲染完毕时调用。作用:进行统一的异常处理,日志处理等。
    void afterCompletion(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4) throws Exception;
}

注册拦截器

编写一个类继承WebMvcConfigurationSupport(或实现 WebMvcConfigurer 接口),并重写addInterceptors来注册拦截器。
多个拦截器的执行顺序取决于拦截器注册的顺序。

应用场景

  • 登录验证,判断用户是否登录

  • 权限验证,判断用户是否有权限访问资源,比如校验token

  • 日志记录,记录请求操作日志(用户IP,访问时间等),以便统计请求访问量

  • 处理cookie、本地化、国际化、主题等

  • 性能监控,监控请求处理时长等

  • 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有比如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)

示例

TestLogInterceptor
MDDTestController
TestWebMvcConfigurer

原理

  • 基于Java反射机制实现的。更准确的划分是基于JDK实现的动态代理。它依赖于具体的接口,在运行期间动态生成字节码。
  • 动态拦截Action调用的对象,在一个Action执行的前后执行一段代码,也可以在一个Action执行前阻止其执行,同时也提供了一种可以提取Action中可重用部分代码的方式。
  • 在AOP中,拦截器用于在某个方法或者字段被访问之前,进行拦截然后再之前或者之后加入某些操作。java的拦截器主要是用在插件上,扩展件上比如 Hibernate Spring Struts2等,有点类似面向切片的技术,在用之前先要在

(拦截器,过滤器,监听器)

mybatis拦截器

应用背景:分页操作,数据权限过滤操作,SQL执行时间性能监控等等, 拦截sql,达到不入侵原有代码业务处理一些东西,用到Mybatis的拦截器Interceptor

从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:

  • Configuration 初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如,插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中
  • SqlSessionFactory SqlSession工厂
  • SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
  • Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  • ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
  • ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  • TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
  • MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装,
  • SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
  • BoundSql 表示动态生成的SQL语句以及相应的参数信息

执行过程
Mybatis拦截器原理

Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理

默认拦截器,自定义拦截器

顺序:

  1. Executor:拦截执行器的方法。
  2. ParameterHandler:拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler:拦截Sql语法构建的处理。

拦截器的原理核心的内容在于拦截器配置解析、拦截器的职责链构造、拦截器的执行,串联上述功能后就能传统整体的流程。

配置文件解析

通过XML配置,原始的XMLConfigBuilder 构建configuration添加拦截器

  • 注册拦截器是通过在Mybatis配置文件中plugins元素下的plugin元素来进行的。
  • 一个plugin对应着一个拦截器,在plugin元素下面可以指定若干个property子元素。
  • Mybatis在注册定义的拦截器时会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。

拦截器链,初始化配置文件的时候就把所有的拦截器添加到拦截器链中

org.apache.ibatis.plugin.InterceptorChain

  • 负责解析XML配置文件解析所有的拦截器对象保存到拦截器链InterceptorChain 。
  • InterceptorChain通过pluginAll植入拦截器。

pluginAll:织入拦截器 – 循环调用每个Interceptor.plugin方法,添加拦截器

addInterceptor:保存拦截器

getInterceptors:获取拦截器

  • 拦截器织入依赖于JDK自带的Proxy.newProxyInstance动态代理来实现。

  • 返回的对象是个Plugin对象的动态代理。

  • 通过解析注解@Intercepts注解@Signature来构建class + method两个维度的map对象。

  • 以@Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class)为例,会构建key为ParameterHandler,value为setParameters的map

针对ParameterHandler 、ResultSetHandler 、StatementHandler 、Executor 的拦截器的织入都是通过interceptorChain.pluginAll来实现。

mybatis 在实例化Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口对象的时候调用interceptorChain.pluginAll() 方法插入进去的。

其实就是循环执行拦截器链所有的拦截器的plugin() 方法,mybatis官方推荐的plugin方法是Plugin.wrap() 方法,这个类就是上面的TargetProxy类

wrap织入单个拦截器

org.apache.ibatis.session.Configuration

产生3种执行器 BatchExecutor/ReuseExecutor/SimpleExecutor

拦截器执行

public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 判断是否是被拦截的方法,如果是就通过拦截器进行处理
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        // 由拦截器interceptor负责处理
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

PageHelper实现

查询方法:PageHelper.startPage(pageNum, pageSize, orderBy);

创建Page对象,后调用查询数据库方法查询

PageInterceptor
String getPageSql(MappedStatement var1, BoundSql var2, Object var3, RowBounds var4, CacheKey var5);

常见问题及注意事项:

1.PageHelper.startPage方法要写在查询语句的前面,

getByPageHelperLocation

startPage()方法调用了Thread.currentThred()方法,也就是说,使用startPage()方法,会在该方法下的第一条sql语句执行分页操作,这也就是为什么导致两条sql分页不起作用的原因,最后,建议将startPage()方法放在service实现层,即所需要的结果的sql之前。

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }

        setLocalPage(page);
        return page;
    }

protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

PageHelper.startPage 对最近的查询语句生效

2.不安全分页

PageHelper 使用了静态的 ThreadLocal 参数,让线程绑定了分页参数, 这个参数如果没被使用就会一直留在那儿,当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

unsafePageHelper

让这个分页参数紧跟查询,可以查就建立;也就是保证被消费;或者在 finally 调用 PageHelper.clearPage(); 清除。

PageHelper使用了静态的ThreadLocal参数,分页参数和线程是绑定的;当分页参数没有被消费时,会一直存在threadlocal中,在下一次执行的sql中会拼接这些参数。

分页参数紧跟 list 查询。如果先写分页,又写了别的判断逻辑,没有执行 list 查询时,那么分页参数就会在threadlocal中,下次执行sql会消费这些参数,就会导致“不安全分页”。

3.调接口分页查询最好使用最后一页标识判断, 否则分页可能死循环
4.对查出来的结果操作时,会影响page的参数,重新创建Page时,需要copy属性

错误原因:丢失了真实类型,计算总记录条数的时候 ,计算了lis的个数,没有从page对象中获取总记录数

改动:创建新的PageInfo对象需要拷贝里面的 属性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值