zoukankan      html  css  js  c++  java
  • Mybatis利用Intercepter实现物理分页

    一、Interceptor介绍

    Mybatis 允许用户使用自定义拦截器对SQL语句执行过程中的某一点进行拦截。默认情况,可以拦截的方法如下:

    1. Executor 中的 update()、query()、flushStatement()、commit()、rollback()、getTransaction()、close()、isClosed()方法。
    2. ParameterHandler 中的getParameterObject()方法、setParameters()方法。
    3. ResultSetHandler 中的 handleResultSets()方法、handleOutputParameters()方法。
    4. StatementHandler 中的 prepare() 方法、parameterize()方法、batch()方法、update()方法、query()方法。

    Interceptor接口如下:

    public interface Interceptor {
        // 执行拦截逻辑的方法
      Object intercept(Invocation invocation) throws Throwable;
        // 决定是否触发intercept()方法
      Object plugin(Object target);
        // 根据配置初始化Interceptor对象
      void setProperties(Properties properties);
    
    }
    

    setProperties()方法可以加载mybatis-config.xml配置文件中配置的属性,例如:

    1. <plugins> 
    2. <plugin interceptor="cn.sp.interceptor.PageInterceptor"> 
    3. <property name="testProp" value="100"></property> 
    4. </plugin> 
    5. </plugins> 

    用户自定义拦截器的plugin()方法可以使用Mybatis提供的Plugin工具类实现,它实现了InvocationHandler接口,并提供了一个wrap()静态方法用于创建代理对象。

    用户自定义的拦截器除了要实现Interceptor接口外,还需要使用 @Intercepts@Signature 注解。
    @Intercepts注解中是一个@Signature列表,每个@Signature注解都标识了该插件需要拦截的方法的信息,其中type表示需要拦截的类型,method属性指定具体的方法名,args属性指定了被拦截方法的参数列表。通过这三个属性值就可以表示一个方法签名。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Signature {
      Class<?> type();
    
      String method();
    
      Class<?>[] args();
    }
    

    二、实现PageInterceptor

    2.1Mybatis的默认分页机制

    Mybatis本身可以使用RowBounds方式进行分页,但是在 DefaultResultSetHandler 中它用的是查询所有数据,然后调用ResultSet.absoulte()方法或循环调用ResultSet.next()方法定位到指定的记录行。这种基于内存分页的方式,当表中的数据量比较大时,会查询全表导致性能问题。
    还有一个就是写SQL基于limit实现的物理分页,但是这种基于 "limit offset,length" 的方式如果offset的值很大时,也会导致性能很差,有时间再详细说说mysql分页部分。

    下面的例子就是通过自定义拦截器实现物理分页。

    2.2代码部分

    搭建一个整合Mybatis的SpringBoot项目很简单,过程我就省略了。
    PersonDao

    public interface PersonDao {
    
      List<Person> queryPersonsByPage(RowBounds rowBounds);
    }
    

    PersonMapper.xml

    1. <?xml version="1.0" encoding="UTF-8" ?> 
    2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > 
    3. <mapper namespace="cn.sp.dao.PersonDao" > 
    4.  
    5. <select id="queryPersonsByPage" resultType="cn.sp.bean.Person"> 
    6. select * from person ORDER BY id DESC 
    7. </select> 
    8. </mapper> 

    PageInterceptor

    /**
     * 利用拦截器实现分页
     * Created by 2YSP on 2019/7/7.
     */
    @Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
            RowBounds.class, ResultHandler.class})
    })
    @Slf4j
    public class PageInterceptor implements Interceptor {
    
      /**
       * Executor.query()方法中,MappedStatement对象在参数列表中的索引位置
       */
      private static int MAPPEDSTATEMENT_INDEX = 0;
    
      /**
       * 用户传入的实参对象在参数列表中的索引位置
       */
      private static int PARAMTEROBJECT_INDEX = 1;
      /**
       * 分页对象在参数列表中的索引位置
       */
      private static int ROWBOUNDS_INDEX = 2;
    
    
      /**
       * 执行拦截逻辑的方法
       */
      @Override
      public Object intercept(Invocation invocation) throws Throwable {
        // 参数列表
        Object[] args = invocation.getArgs();
        final MappedStatement mappedStatement = (MappedStatement) args[MAPPEDSTATEMENT_INDEX];
        final Object parameter = args[PARAMTEROBJECT_INDEX];
        final RowBounds rowBounds = (RowBounds) args[ROWBOUNDS_INDEX];
        // 获取offset,即查询的起始位置
        int offset = rowBounds.getOffset();
        int limit = rowBounds.getLimit();
        // 获取BoundSql对象,其中记录了包含"?"占位符的SQL语句
        final BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        // 获取BoundSql中记录的SQL语句
        String sql = boundSql.getSql();
        sql = getPagingSql(sql, offset, limit);
        log.info("==========sql:
    " + sql);
        // 重置RowBounds对象
        args[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
        // 根据当前语句创建新的MappedStatement
        args[MAPPEDSTATEMENT_INDEX] = createMappedStatement(mappedStatement, boundSql, sql);
        // 通过Invocation.proceed()方法调用被拦截的Executor.query()方法
        return invocation.proceed();
      }
    
      private Object createMappedStatement(MappedStatement mappedStatement, BoundSql boundSql,
          String sql) {
        // 创建新的BoundSql对象
        BoundSql newBoundSql = createBoundSql(mappedStatement, boundSql, sql);
        Builder builder = new Builder(mappedStatement.getConfiguration(), mappedStatement.getId(),
            new BoundSqlSqlSource(newBoundSql), mappedStatement.getSqlCommandType());
        builder.useCache(mappedStatement.isUseCache());
        builder.cache(mappedStatement.getCache());
        builder.databaseId(mappedStatement.getDatabaseId());
        builder.fetchSize(mappedStatement.getFetchSize());
        builder.flushCacheRequired(mappedStatement.isFlushCacheRequired());
    
        builder.keyColumn(delimitedArrayToString(mappedStatement.getKeyColumns()));
        builder.keyGenerator(mappedStatement.getKeyGenerator());
        builder.keyProperty(delimitedArrayToString(mappedStatement.getKeyProperties()));
    
        builder.lang(mappedStatement.getLang());
        builder.resource(mappedStatement.getResource());
    
        builder.parameterMap(mappedStatement.getParameterMap());
        builder.resultMaps(mappedStatement.getResultMaps());
        builder.resultOrdered(mappedStatement.isResultOrdered());
        builder.resultSets(delimitedArrayToString(mappedStatement.getResultSets()));
        builder.resultSetType(mappedStatement.getResultSetType());
    
        builder.timeout(mappedStatement.getTimeout());
        builder.statementType(mappedStatement.getStatementType());
    
        return builder.build();
    
      }
    
      public String delimitedArrayToString(String[] array) {
        String result = "";
        if (array == null || array.length == 0) {
          return result;
        }
        for (int i = 0; i < array.length; i++) {
          result += array[i];
          if (i != array.length - 1) {
            result += ",";
          }
        }
        return result;
      }
    
      class BoundSqlSqlSource implements SqlSource {
    
        private BoundSql boundSql;
    
        public BoundSqlSqlSource(BoundSql boundSql) {
          this.boundSql = boundSql;
        }
    
        @Override
        public BoundSql getBoundSql(Object parameterObject) {
          return boundSql;
        }
      }
    
      private BoundSql createBoundSql(MappedStatement mappedStatement, BoundSql boundSql, String sql) {
        BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql,
            boundSql.getParameterMappings(), boundSql.getParameterObject());
        return newBoundSql;
      }
    
      /**
       * 重写sql
       */
      private String getPagingSql(String sql, int offset, int limit) {
        sql = sql.trim();
        boolean hasForUpdate = false;
        String forUpdatePart = "for update";
        if (sql.toLowerCase().endsWith(forUpdatePart)) {
          // 将当前SQL语句的"for update片段删除"
          sql = sql.substring(0, sql.length() - forUpdatePart.length());
          hasForUpdate = true;
        }
    
        StringBuilder result = new StringBuilder();
        result.append(sql);
        result.append(" limit ");
        result.append(offset);
        result.append(",");
        result.append(limit);
    
        if (hasForUpdate) {
          result.append(" " + forUpdatePart);
        }
        return result.toString();
      }
    
      /**
       * 决定是否触发intercept()方法
       */
      @Override
      public Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      /**
       * 根据配置初始化Interceptor对象
       */
      @Override
      public void setProperties(Properties properties) {
        log.info("properties: " + properties.getProperty("testProp"));
      }
    }
    
    

    这里的思路就是拦截 query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) 方法或query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) 方法,通过RowBounds对象获得所需记录的offset和limit,通过BoundSql获取待执行的sql语句,最后重写SQL语句加入"limit offset,length"实现分页。

    三、测试

    执行如下测试方法:

     

    分页测试
    分页测试

    控制台显示结果:

     

     

    enter description here
    enter description here

    输出结果是,id为7的王五和id为6的张三,再对比数据库数据。

     

     

    enter description here
    enter description here

    最后得出结论成功实现分页查询,GitHub上开源的大名鼎鼎的PageHelper也是利用拦截器插件实现的,有时间要再看下它的源码实现。
    本文代码点击这里

     

  • 相关阅读:
    (转)JQuery中$.ajax()方法参数详解
    __dopostback的用法 . 编辑
    (转)__dopostback的用法 .
    (转)如何区分一个程序员是“老手“还是“新手“?
    jQuery验证框架 .
    location.href的用法
    为用户设置密码
    设置环境变量
    用 xampp 在ubuntu 下配置php 运行环境 lampp
    安装与配置JDK
  • 原文地址:https://www.cnblogs.com/2YSP/p/11166771.html
Copyright © 2011-2022 走看看