mybatis流式查询中Cursor和ResultHandler的区别

2,000 阅读3分钟

介绍下是什么

  1. 这两个都可以用于流式查询,简单说下,流式查询可以在查询大量数据时候,防止内存爆掉。 简单点说,一次查100W条数据,JVM堆栈设置100M,如果使用流式查询,是不会出现内存溢出的。这两个的具体用法可以参考其他博客,这里就不重复垃圾话了。主要是说说这两有什么区别。

区别是什么?

  1. Cursor和ResultHandler功能是一样的,Cursor比ResultHandler多包装了一层,实际Cursor调用时候,还是和ResultHandler调用的逻辑一样。具体我们看源码:

ResultHandler的逻辑

  1. 判断走哪个逻辑查询
case SELECT:
    if (method.returnsVoid() && method.hasResultHandler()) {
    //查询ResultHandler会走这个逻辑
        "executeWithResultHandler(sqlSession, args);"
        result = null;
    } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
        //以下代码省略
  1. 下一步调用这个方法
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  //查询数据库获取游标数据的方法
  ps.execute();
  //进入handleResultSets这个方法
  return resultSetHandler.handleResultSets(ps);
}
  1. 一路往下,最终会调用这个方法
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
    throws SQLException {
  DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
  ResultSet resultSet = rsw.getResultSet();
  skipRows(resultSet, rowBounds);
  //主要看这里,这个循环,看见resultSet.next()这个方法没有,是不是很熟悉?原生调用JDBC查询数据时候,会有这个一个方法返回,每次next表示查看下一个游标对应的数据还有没有,如果有就取出来,没有就是结束,查完了。
  while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
    ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
    Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
  }
}
  1. 具体的执行方法在这里
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
//resultContext是存放数据的上下文对象
  resultContext.nextResultObject(rowValue);
  //这个是在哪个方法里取出上下文数据做处理的方法,有点拗口。
  //其实就是你用ResultHandler查询时候,要写一个实现类做数据处理
  //这个实现类就是在这个时候被调用的,通过这个方法调用,数据在哪里?
  //看见resultContext没有,这个类里就装了你的数据,并且是一条条数据的做处理。
  //处理完后,会被后一条数据替换。这个就是流式查询最核心的地方。
  //我们平时用得最多的list查询,其实只是把查询结果装在一个list里而已。有兴趣的老铁可以打断点自己看看
  ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}

Cursor的逻辑:

  1. 判断走哪个逻辑查询
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()) {
    //查询Cursor会走这个逻辑
        "result = executeForCursor(sqlSession, args);"
    }
      //以下代码省略
  1. 这个方法是主要执行的方法
@Override
public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  //去数据库查询所有游标数据,得到的数据放在ps上
  ps.execute();
  //传入ps,执行下面这个方法
  return resultSetHandler.handleCursorResultSets(ps);
}

下一步,执行这个方法

@Override
public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId());

  ResultSetWrapper rsw = getFirstResultSet(stmt);

  List<ResultMap> resultMaps = mappedStatement.getResultMaps();

  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  if (resultMapCount != 1) {
    throw new ExecutorException("Cursor results cannot be mapped to multiple resultMaps");
  }

  ResultMap resultMap = resultMaps.get(0);
  //主要看这里,返回值中,创建了Cursor接口的实现类,并传入对应数据,这些数据肯定会通过构造器赋值给这个类里的属性。
  return new DefaultCursor<>(this, resultMap, rsw, rowBounds);
}
  1. 下一步。
//这个是Cursor实现类的构造器
public DefaultCursor(DefaultResultSetHandler resultSetHandler, ResultMap resultMap, ResultSetWrapper rsw, RowBounds rowBounds) {
//主要看这个属性,resultSetHandler,把屏幕拉到最上面,找到属性名resultSetHandler
  this.resultSetHandler = resultSetHandler;
  this.resultMap = resultMap;
  this.rsw = rsw;
  this.rowBounds = rowBounds;
}
  1. 关键还是这三个属性
//这个是不是很眼熟,最终还是调用这个属性resultSetHandler里的方法处理数据
private final DefaultResultSetHandler resultSetHandler;
//当前类DefaultCursor可以写泛型,那么泛型类对应的属性就是objectWrapperResultHandler,也就是你PO的对象(也可能是DTO等)
protected final ObjectWrapperResultHandler<T> objectWrapperResultHandler = new ObjectWrapperResultHandler<>();
//迭代器,是一个内部类,本类下面有方法,把获取的数据放进objectWrapperResultHandler里。
private final CursorIterator cursorIterator = new CursorIterator();
  1. 所以最终调用获取数据的方法还是这个方法。和ResultHandler没太大区别,所以个人认为Cursor只是多包装了一层而已。
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
  resultContext.nextResultObject(rowValue);
  ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}

总结:

  1. 在使用的时候,Cursor需要自己关闭连接,具体怎么执行,可查看其他博客
  2. ResultHandler使用时要注意,fetchSize字段的值根据需要设置,如果一次查询的数据量没超过堆栈上限,可以设置100-1000之间的值(具体看业务)提高查询效率,这个值的意思是,每次查询从数据库一次取多少条数据,然后存放在内存里,然后在通过从内存拿这个数据。
  3. 如果要实现查询百万以上数据,还不想被爆内存可以把fetchSize设置为Integer.MIN_VALUE,具体为什么,我也没搞清楚,因为涉及要去数据库底层看逻辑,能力有限,暂时没搞明白。
  4. Cursor要实现怎么查询百万以上的数据,目前也没搞明白。
  5. 能力有限,写得不好的地方希望指出,感谢观看的老铁们。如果有大佬路过,希望能指点下,感谢。