【備忘】MyBatisのSelectCursorではNestedResultMapは利用できない

MyBatisのCursor機能で嵌ったのでメモ。

## 検証環境
- MyBatis-3.4.4
- PostgreSQL 9.4

## NestedResultMapとは
hasOneやhasManyの関係にあるデータを効率的にJavaBeanにマッピングするための方法。
JOINした結果を使ってマッピングすることができるので「N+1セレクト問題」に対応できる。

## SelectListとSelectCursorの違い
以下の簡単なテーブル(userとuser_tel)を使って検証

|user_id|  name |
|:------|------:|
| 1     |   AAA |
| 2     |   BBB |
| 3     |   CCC |

|user_id|sub_number|  tel  |
|:------|:---------|------:|
| 1     |1         | Atel1 |
| 2     |1         | Btel1 |
| 2     |2         | Btel2 |
| 3     |1         | Ctel1 |
| 3     |2         | Ctel2 |
| 3     |3         | Ctel3 |

Mapper.xmlは以下の通り

```xml:test.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
  <resultMap id="UserResult" type="User">
    <id property="userId" column="user_id"/>
    <result property="name" column="name" />
    <collection property="tels" ofType="String">
      <id column="tel"/>
    </collection>
  </resultMap>
  <select id="selectTest" resultMap="UserResult">
   SELECT
     u.user_id,
     u.name,
     ut.tel
   FROM testuser.user u
   INNER JOIN testuser.user_tel ut ON u.user_id = ut.user_id
  </select>
</mapper>
```

```Test.java
	private static final String SQLMAP =
			"sandbox/example/sqlMap.xml";
	private static final String MAPPER =
			"test.selectTest";
    public static void main( String[] args ) throws IOException
    {
    	SqlSession session;
    	SqlSessionFactory factory;
    	try(Reader reader = Resources.getResourceAsReader(SQLMAP)){
    		factory = new SqlSessionFactoryBuilder().build(reader);
    	}
    	session = factory.openSession();
    	List<User> result = session.selectList(MAPPER);
		for(User dto : result) {
			System.out.println(String.format("%s:%d", dto.getUserId(),dto.getTels().size()));
		}
    	try(Cursor<User> cur = session.selectCursor(MAPPER)){
    		for(User dto : cur) {
    			System.out.println(String.format("%s:%d", dto.getUserId(),dto.getTels().size()));
    		}
    	}
    	session.close();
    }
```
実行結果は下記の通り

```
1:1
2:2 <- selectListはきちんと2件とれてる
3:3 <- selectListはきちんと3件とれてる
1:1
2:1 <- selectCursorは1件のみ
3:1 <- selectCursorは1件のみ
```

##原因
原因としてはMyBatisのhandleRowValuesForNestedResultMapというメソッドにおいてCursorの場合にループが上手く回らないことだと分かった。
以下に抜粋する。

```java:org.apache.ibatis.executor.resultset.DefaultResultSetHandler
  private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    final DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
    skipRows(rsw.getResultSet(), rowBounds);
    Object rowValue = previousRowValue;
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
      final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
      final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
      Object partialObject = nestedResultObjects.get(rowKey);
      // issue #577 && #542
      if (mappedStatement.isResultOrdered()) {
        if (partialObject == null && rowValue != null) {
          nestedResultObjects.clear();
          storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
        }
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
      } else {
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
        if (partialObject == null) {
          storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
        }
      }
    }
    if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
      storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
      previousRowValue = null;
    } else if (rowValue != null) {
      previousRowValue = rowValue;
    }
  }

  private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
    if (parentMapping != null) {
      linkToParents(rs, parentMapping, rowValue);
    } else {
      callResultHandler(resultHandler, resultContext, rowValue);
  }
  
  @SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object>*/)
  private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
    resultContext.nextResultObject(rowValue);
    ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
  }
    private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
  }
```

while文の中で取得した結果に対して親が存在するかどうかを判断して、親がいれば紐づけていく処理になっている。
この処理の中で
1. storeObject()を呼び出し
2. parentMappingがNullのケースではcallResultHandler()を呼び出し
3. resultHandleのhandleResultを実行
4. shouldProcessMoreRowsで続行判断
という流れでマッピングが進んでいます。

selectListとselectCursorではこの中の「resultHandleのhandleResult」の処理が異なっていることが分かりました。
selectListでは以下のDefaultResultHandlerを利用しています。

```java:org.apache.ibatis.executor.result.DefaultResultHandler
  public void handleResult(ResultContext<? extends Object> context) {
    list.add(context.getResultObject());
  }
```

一方selectCursorは、以下のObjectWrapperResultHandlerを利用しています

```Java:org.apache.ibatis.cursor.defaults.DefaultCursor
    private static class ObjectWrapperResultHandler<T> implements ResultHandler<T> {

        private T result;

        @Override
        public void handleResult(ResultContext<? extends T> context) {
            this.result = context.getResultObject();
            context.stop();
        }
    }
```

このため、selectCursorの場合は「handleResult」を呼ばれた時点で「context.stop()」が呼び出されてしまうため、「shouldProcessMoreRows」で処理が終了してしまうため必ず1件しかリストに格納されない状態で処理がリターンされます。
user_id=2→3に行くときの処理は以下の通りです。
1. user_id=2,Btel2のレコードを処理（user_id=2のtelsにBtel1,Btel2が入った状態)
2. user_id=3,Ctel1のレコードを処理（idが変わったので新たなオブジェクトを生成)
3. ループ処理を抜けて「user_id=3のオブジェクトのみ返却」

またuser_id=3のオブジェクトを返却したあともCursorは残っているので
1. user_id=3,Ctel2のレコードを処理 (user_id=3のtelsにCtel1,Ctel2が入った状態）
2. user_id=3,Ctel3のレコードを処理 (user_id=3のtelsにCtel1,Ctel2,Ctel3が入った状態）
3. ループ処理を抜ける(新たなidのオブジェクトはないので返却無し)
となります。

## まとめ

バッチで大量件数の処理に耐えられるようにするため。
- SQLの発行回数の削減(N+1問題の回避)
- Cursorを利用してListに格納しない(メモリ使用量の抑止)
の両立をしようとしましたが、現時点では無理だということが分かりました。
Javadocにも「A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.」とあったので出来るものかと試行錯誤してみましたが駄目でした。
Cursorだから1件ずつが当たり前！なのかもしれませんが、嵌っている方のお役に立てれば幸いです。