mybatis

mybatis源码分析-selectOne-05

2019-10-13  本文已影响0人  愤怒的奶牛

上篇文章我们分析到 PreparedStatementHandler,这篇文章我们就来详细的分析下 PreparedStatementHandler。

1.1 PreparedStatementHandler
/**
 * @author Clinton Begin
 */
public class PreparedStatementHandler extends BaseStatementHandler {

  public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
  }

  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

  @Override
  public void batch(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.addBatch();
  }
// delegate.query(statement, resultHandler); 这句话就是调用的这句话
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute(); // sql 语句执行
    return resultSetHandler.handleResultSets(ps);  // 遍历查询结果集。
  }

  @Override
  public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleCursorResultSets(ps);
  }

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }

  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute(); // sql 语句执行
    return resultSetHandler.handleResultSets(ps);  // 遍历查询结果集。
  }

上面这段代码就是去数据库中执行查询语句,并将结果集 的封装交给了 resultSetHandler 接口,接下来我们就来看看 ResultHandler 接口。

1.2 ResultHandler
/**
 * @author Clinton Begin
 */
public interface ResultSetHandler {
// 传入 Statement  对象,返回 List 
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

ResultSetHandler 接口就只有一个实现类 DefaultResultSetHandler

1.2.1 DefaultResultSetHandler
public class DefaultResultSetHandler implements ResultSetHandler {
....
//
  // HANDLE RESULT SETS 查询 结果集的封装
  //
  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
   // 到这里我们就知道了 我们查询结果的结果集是 一个 ArrayList ,本来这个没有啥问题,但是 要是和 pagehelper 一起 使用的话,可能会发生 分页的时候记录总数 错误的情况,后面我们会贴一段 pagehelper 的代码。
    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
// 遍历处理每一行记录,并添加到 multipleResults 中
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);// 返回 multipleResults 对象。
  }
 @SuppressWarnings("unchecked")
  private List<Object> collapseSingleResultList(List<Object> multipleResults) {
    return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
  }
.....
}

上面一段代码是对结果集的封装,方法里面有嵌套了其他方法来处理对象映射,这里我们就不细看里面的方法,我们会在后面来详细分析 参数映射 和结果集映射。这里我们的主线任务是追踪 查询过程,到这里我们就完成了主线任务。我们上面提到了 在使用 pagehelper 中会出现 查询总数错误的情况,结合上面的代码和 pagehelper 的代码我们来分析下原因。

1.3 pagehelper 分页示例
    @Test
    public void pageList() {
// PageHelper 分页要求分页的语句紧跟   PageHelper.startPage(1,10); 而且只对 第一条语句有效
        PageHelper.startPage(1,10);
  // 这里得到是Page 对象,Page对象 继承 ArrayList
        List<User> userList = userMapper.selectAll();
        PageInfo<User> userPageInfo = new PageInfo<>(userList);
    }

上面是正常能获取到分页 数据的最简单写法,但是在很多时候我们需要 处理 List<User> userList = userMapper.selectAll(); 这个结果集,比如 会过滤掉 用户的密码,那么我们常规的做法可能 创建一个 UserVo 对象,把需要展示的 字段设置到 UserVo 对象里面去。
下面这种方式就会返回错误的分页数据:

 @Test
    public void pageListUserVo() {
        PageHelper.startPage(1,2);
     
        List<User> userList = userMapper.selectAll();
        List<UserVo> userVoList = userList.stream().map(item -> UserVo.builder().username(item.getUsername()).id(item.getId()).build()).collect(Collectors.toList());
        PageInfo<UserVo> userPageInfo = new PageInfo<>(userVoList);
        int pages = userPageInfo.getPages();
        long total = userPageInfo.getTotal();
        System.out.println("pages = "+ pages);//总页数
        System.out.println("total = "+ total);// 总的记录数
    }

上面这种写法会经常在我们的开发中出现,首先我们来分析下出现错误的分页数据的原因。首先我们确定 发出的sql 语句是正确的 : Preparing: select id, username, password from sys_user LIMIT ? 。那么就是在 结果封装的时候出现了错误,也就是 new PageInfo<>()。

1.4 PageInfo
....
 /**
     * 包装Page对象
     *
     * @param list
     */
    public PageInfo(List<T> list) {
        this(list, 8);
    }

    /**
     * 包装Page对象
     *
     * @param list          page结果
     * @param navigatePages 页码数量
     */
    public PageInfo(List<T> list, int navigatePages) {
        super(list);
        if (list instanceof Page) { // 如果 list 是Page 对象的话执行这里,显然第一种分页是走的这里 
        //PageHelper.startPage(1,10);
        //List<User> userList = userMapper.selectAll(); // 返回的是 Page 对象。
        //PageInfo<User> userPageInfo = new PageInfo<>(userList);
            Page page = (Page) list;
            this.pageNum = page.getPageNum();
            this.pageSize = page.getPageSize();
// 总页数和总的记录数
            this.pages = page.getPages();
            this.size = page.size();
            //由于结果是>startRow的,所以实际的需要+1
            if (this.size == 0) {
                this.startRow = 0;
                this.endRow = 0;
            } else {
                this.startRow = page.getStartRow() + 1;
                //计算实际的endRow(最后一页的时候特殊)
                this.endRow = this.startRow - 1 + this.size;
            }
        } else if (list instanceof Collection) { // 第二次的User->UserVo 显然是这里 Collectors.toList();返回的是 是  Collection接口,这里的分页总数都是通过 集合计算的
            this.pageNum = 1;
            this.pageSize = list.size();

            this.pages = this.pageSize > 0 ? 1 : 0;// 总页数 1或者 0
            this.size = list.size();// 总记录数是 集合的大小。
            this.startRow = 0;
            this.endRow = list.size() > 0 ? list.size() - 1 : 0;
        }
        if (list instanceof Collection) {
            this.navigatePages = navigatePages;
            //计算导航页
            calcNavigatepageNums();
            //计算前后页,第一页,最后一页
            calcPage();
            //判断页面边界
            judgePageBoudary();
        }
    }
public class Page<E> extends ArrayList<E> implements Closeable {
    private static final long serialVersionUID = 1L;

    /**
     * 页码,从1开始
     */
    private int pageNum;
    /**
     * 页面大小
     */
    private int pageSize;
    /**
     * 起始行
     */
    private int startRow;
    /**
     * 末行
     */
    private int endRow;
...

到这里我们就分析出了 为什么在 使用 分页插件的时候 会出现分页数据错误的情况,主要原因是 我们在User->UserVo 的时候 结果集 List 的类型发生了变化,插件返回的List 集合 其实是 Page 对象 ,而我们在转换了 后 返回的 是 以合 List 接口,然后进行 new PageInfo() 的时候,里面算出来的就不是我们想要的数据了。那么我们要怎么做呢?最简单的方式就是 我们在 User->UserVo 的时候也返回 Page 对象 比如这样: Page pageList = new Page(userVoList)。但是我们看完了 Page 对象 的所有方法包括构造器 ,都没有一个 方法可以让我们自己 设置 结果集 ,这点的话 springData jpa 就比较好,接口设计比较合理。当然我们也还有其他方式:(均来自 网络博客)

  1. mapper 对象 查询的时候 直接放回 xxxVo 对象。但是这种方式 可能导致的方法的复用性较低,不过也无可厚非。
  2. 结果集 查询先 放入 PageInfo 对象中,然后 Copy 数据到新的 PageInfo 对象。这种方式 可以复用 mapper 的查询接口,但是 一定程度上加大了代码的复杂度,不是很优雅。
    @Test
    public void pageListUserVo2() {
        PageHelper.startPage(1, 2);
        List<User> userList = userMapper.selectAll();
        PageInfo<User> userPageInfo = new PageInfo<>(userList);// 先保存数据

        List<UserVo> userVoList = userList.stream().map(item -> UserVo.builder().id(item.getId()).username(item.getUsername()).build())
                .collect(Collectors.toList());
        PageInfo<UserVo> userVoPageInfoFinal = new PageInfo<>(userVoList);

        // 这里就是分页数据的 Copy 如总的页数,总的记录数
        BeanUtils.copyProperties(userPageInfo, userVoPageInfoFinal);

        int pages = userVoPageInfoFinal.getPages();
        long total = userVoPageInfoFinal.getTotal();
        System.out.println("pages = "+ pages);//总页数
        System.out.println("total = "+ total);// 总的记录数
    }

上面的思路很简单,先把分页数据保存下来,类型转换完后,复制 原来正确的分页数据到 最后的 PageInfo 对象中。如果还有其他处理方式,欢迎留言交流。

本片文章分析了 selectOne 最后的结果集封装,我们知道了 原生的 mybatis 结果是 封装到一个 ArrayList 中的,后面我们会详细分析 mybatis 的参数映射和 结果集的封装。此外我们还分析了 PageHelper 分页数据错误的原因和 解决方案,请持续关注后续!

上一篇 下一篇

猜你喜欢

热点阅读