代码编织梦想

今天处理分页查询时,报PersistenceException。查看控制台发现,sql中没有获取到分页参数,而且多了一个用来查询总数queryUpfile_Count方法。

在这里插入图片描述
反复确认了传参无问题后,最后查看xml发现sql语句有拼接limit #{pageNo}, #{pageSize}。这时候我就想,原有项目中使用PageHelper做分页,是不是凭借的sql语句与PageHelper产生冲突呢?带着这个疑问我就来查看PageHelper的源码。

<!-- 查询上传文件记录 -->
<select id="queryUpfile" resultType="IotMsisdnUpfile">
  select <include refid="Base_Column_List"/>
  from iot_msisdn_upfile
  limit #{pageNo}, #{pageSize}; 
</select>

一般情况下我们使用PageHelper时,都会先使用startPage(),后面紧跟着一个查询语句,进而开始分页功能。

PageHelper.startPage(pageNo, pageSize);
List<IotMsisdnUpfile> list = iotMsisdnUpfileMapper.queryUpfile(pageNo, pageSize);

我们使用debug模式进入PageMethod中的startPage方法,一路往下点来到如下面所示的方法。发现我们传递pageNo和pageSize保存到Page中,并设置到setLocalPage(page)方法中。

/**
 * 开始分页
 *
 * @param pageNum      页码
 * @param pageSize     每页显示数量
 * @param count        是否进行count查询
 * @param reasonable   分页合理化,null时用默认配置
 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
 */
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page<E>(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    //当已经执行过orderBy的时候
    Page<E> oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    setLocalPage(page);
    return page;
}

点开setLocalPage()之后,恍然大悟,原来page保存到ThreadLocal中,用来保证变量的线程私有,确认线程安全问题。

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

/**
 * 设置 Page 参数
 *
 * @param page
 */
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

我们知道,ThreadLoacal中有set(),必然有get()。这时候我们就猜想MyBatis中肯定使用了get()方法获取到Page对象,并取出我们传入pageNo和pageSize,从而实现分页查询。
我们继续往下面debug,来到MapperProxy中的invoke()方法,并且执行了execute()方法。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

来到MapperMethod的execute()方法,我们发现这里使用switch来判断当前执行sql的类型。由于我们是SELECT语句而且返回多个结果,故来到了"SELECT"中的 executeForMany(sqlSession, args)。

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
   Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName() 
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

点开executeForMany(),发现其执行了一个selectList()方法

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  List<E> result;
  Object param = method.convertArgsToSqlCommandParam(args);
  if (method.hasRowBounds()) {
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
  } else {
    result = sqlSession.<E>selectList(command.getName(), param);
  }
  // issue #510 Collections & arrays support
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    } else {
      return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}

点开selectList()之后,我们就来到DefaultSqlSession。发现其返回executor.query()的结果,我们就知道了线程池连接数据库查询的方法就是executor.query()方法。

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

继续点开我们发现executor.query()被代理了,来到Plugin类中invoke()方法。仔细查看invoke()方法发现,这里执行了interceptor.intercept(new Invocation(target, method, args)),返回了一个拦截器。

@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)) {
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

点开intercept()来到了PageInterceptor中,这是我们才发现PageHelper就是通过实现MyBatis拦截器的模式来分页功能。这时我们进入PageInterceptor.intercept()发现如下代码,dialect.skip(ms, parameter, rowBounds)来判断是否需要分页。

//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
    //判断是否需要进行 count 查询
    if (dialect.beforeCount(ms, parameter, rowBounds)) {
        //查询总数
        Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        //处理查询总数,返回 true 时继续分页查询,false 时直接返回
        if (!dialect.afterCount(count, parameter, rowBounds)) {
            //当查询总数为 0 时,直接返回空的结果
            return dialect.afterPage(new ArrayList(), parameter, rowBounds);
        }
    }
    resultList = ExecutorUtil.pageQuery(dialect, executor,
            ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
    //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
    resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}

我们这时进入skip()方法,这时眼睛一亮,终于找到PageHelper.getLocalPage()来获取我们Page对象,并一同返回给拦截器执行我们的查询。

@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
    if (ms.getId().endsWith(MSUtils.COUNT)) {
        throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
    }
    Page page = pageParams.getPage(parameterObject, rowBounds);
    if (page == null) {
        return true;
    } else {
        //设置默认的 count 列
        if (StringUtil.isEmpty(page.getCountColumn())) {
            page.setCountColumn(pageParams.getCountColumn());
        }
        autoDialect.initDelegateDialect(ms);
        return false;
    }
}

/**
 * 获取分页参数
 *
 * @param parameterObject
 * @param rowBounds
 * @return
 */
public Page getPage(Object parameterObject, RowBounds rowBounds) {
    Page page = PageHelper.getLocalPage();
    if (page == null) {
        if (rowBounds != RowBounds.DEFAULT) {
            if (offsetAsPageNum) {
                page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
            } else {
                page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
                //offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
                page.setReasonable(false);
            }
            if(rowBounds instanceof PageRowBounds){
                PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
                page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
            }
        } else if(parameterObject instanceof IPage || supportMethodsArguments){
            try {
                page = PageObjectUtil.getPageFromObject(parameterObject, false);
            } catch (Exception e) {
                return null;
            }
        }
        if(page == null){
            return null;
        }
        PageHelper.setLocalPage(page);
    }
    //分页合理化
    if (page.getReasonable() == null) {
        page.setReasonable(reasonable);
    }
    //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
    if (page.getPageSizeZero() == null) {
        page.setPageSizeZero(pageSizeZero);
    }
    return page;
}

最后,我们知道PageHelper已经帮我封装了pageNo和pageSize,所以我们就可以不用在sql语句中加入limit。所以,我们将sql修改如下,就可以愉快使用PageHelper来实现分页查询。

<!-- 查询上传文件记录 -->2
<select id="queryUpfile" resultType="IotMsisdnUpfile">
    select <include refid="Base_Column_List"/> from iot_msisdn_upfile
</select>

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/kaoya156/article/details/111088967

Java中的基本运算符-爱代码爱编程

运算符 使用方法示例 算术运算符 public class Demo006 { public static void main(String[] args) { int a = 10; int b = 20; int c = 21; //idea快捷键:ctrl+d,复制当

【已解决】Jena配置问题-爱代码爱编程

1、配置jdk、安装jre,网上教程很多,不赘述; 2、下载jena包,配置好环境变量,输入sparql --version出现相应版本号即安装成功; 3、安装eclipse,新建JenaTesting工程,接下来就是很多博客遇到的但未解决的问题: (1)工程右键-->build path-->configure build path,导

IntelliJ IDEA使用教程(动图详解):IntelliJ IDEA 相关核心文件和目录介绍-爱代码爱编程

IntelliJ IDEA 相关核心文件和目录介绍 安装目录介绍 IntelliJ IDEA 的安装目录并不复杂,上图为最常改动的 bin 目录,经常会改动的文件或是必须介绍就是如图红色框中的几个。idea.exe 文件是 IntelliJ IDEA 32 位的可行执行文件,IntelliJ IDEA 安装完默认发送到桌面的也就是这个执行文件

重定向防止表单重复提交-爱代码爱编程

重定向防止表单重复提交 1、表单重复提交的情况:2、实例(1)针对第一种情况(提交完表单之后,刷新网页)(2)针对第二种情况(网络延迟)(3)回退按钮重复提交表单3、重复提交带来的问题 1、表单重复提交的情况: (1)提交完表单之后,刷新网页。 (2)第一次单击提交之后,在没有提交成功情况下,又单击提交按钮。 (3)用户提交表单后,点击浏

JSP九大内置对象-爱代码爱编程

JSP九大内置对象 内置对象可以直接使用,不用new。 pageContext JSP页面容器(一般也称page对象)request 请求对象session 会话对象application 全局对象response 响应对象config 配置对象(服务器配置信息)out 输出对象page 当前jsp页面对象(相当于java中的this)exceptio

静态方法(static)和非静态方法-爱代码爱编程

使用静态方法后,对方法的调用直接 类名.方法名 public class Student { public static void say(){ System.out.println("学生回答了!"); } } public class Demo01 { public static void main(Str

mysql常规使用(建立,增删改查,视图索引)-爱代码爱编程

目录 1.数据库建立 2.增删改查 3.视图建立: 1.数据库建立 mysql> mysql> show databases; +-----------------------------------+ | Database | +-----------------------------

访问阿里云mysql出现Access denied for user ‘root‘@‘xxxxx‘ (using password: YES)-爱代码爱编程

问题: 在我连接远程阿里云的mysql时候,出现了Access denied for user 'root'@'xxxxx' (using password: YES)问题。 排查: 1、密码是否正确 2、阿里云的虚拟机是否开放了3306端口号。 我的就是密码正确,而且开放了3306端口的还是这样的错误,最后发现是权限不够的原因。 修改方法:

数据库SQL语言在MySQL中运用子查询结果插入数据-爱代码爱编程

子查询不仅可以嵌套在SELECT语句中用以构造父查询的条件,也可以嵌套在INSERT语句中用以生成要插入的批量数据插入子查询结果的INSERT语句格式为: # 插入子查询结果 INSERT INTO <表名> [(<属性列1>[,<属性列2>...]) 子查询; 举例: 对每一个班级求学生的平均年龄,并把结果存入数

解决Cent OS 6.5 中yum源错误:YumRepo Error: All mirror URLs are not using ftp, http[s] or file和404-爱代码爱编程

在Linux系统安装MySQL的时候,想要移除系统自带的mysql版本,使用 yum -y remove mysql-libs.x86_64 命令进行移除时报以下错误: YumRepo Error: All mirror URLs are not using ftp, http[s] or file. Eg. Invalid release/repo/

Linux搭建开发环境-爱代码爱编程

Linux搭建开发环境 文章目录 Linux搭建开发环境一、安装 Java二、安装MySQL1、配置YUM源2、安装MySQL3、启动MySQL服务4、开机启动5、修改root本地登录密码6、添加远程登录用户7、配置默认编码为utf8三、安装Tomcat1、准备2、启动 tomcat3、开放腾讯云端口4、重启tomcat5、解决tomcat7启动

使用 DDL 语句分别创建仓库表、供应商表、产品表和入库表,并对其进行操作-爱代码爱编程

查看本章节 查看作业目录 需求说明: 使用 DDL 语句分别创建仓库表、供应商表、产品表和入库表使用 DML 语句分别新增一条仓库表、供应商表、产品表和入库表记录使用 DML 语句更新满足指定条件的产品进货单价删除一条供应商记录(例如“丁供应商”)实现思路: 入库表中字段仓库名(dName)、供应商名(pName)和产品编号(goodsID)分别参