diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index 92cb2598bd8..72694793428 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -846,9 +846,11 @@ private String prependPrefix(String columnName, String prefix) { private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { final DefaultResultContext resultContext = new DefaultResultContext<>(); - skipRows(rsw.getResultSet(), rowBounds); + if ( ! rsw.getResultSet().isClosed()) { + skipRows(rsw.getResultSet(), rowBounds); + } Object rowValue = previousRowValue; - while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { + while (shouldProcessMoreRows(resultContext, rowBounds) && ! rsw.getResultSet().isClosed() && rsw.getResultSet().next()) { final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null); final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); Object partialObject = nestedResultObjects.get(rowKey); diff --git a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java index 6ac6f52d14e..53cf627ef0c 100644 --- a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java +++ b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java @@ -33,6 +33,7 @@ import java.util.List; import org.apache.ibatis.builder.StaticSqlSource; +import org.apache.ibatis.cursor.defaults.DefaultCursor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.ExecutorException; import org.apache.ibatis.executor.parameter.ParameterHandler; @@ -42,6 +43,7 @@ import org.apache.ibatis.mapping.ResultMapping; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultContext; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.TypeHandler; @@ -65,6 +67,82 @@ public class DefaultResultSetHandlerTest { private Connection conn; @Mock private DatabaseMetaData dbmd; + + /** + * Test the behavior of calling {@link DefaultResultSetHandler#handleRowValues} + * multiple times with a nested mapping that has ordered results. + * This test replicates the behavior of the {@link DefaultCursor}. + * + * Some database drivers (like DB2) do not allow any calls to the {@link ResultSet} + * after the {@link ResultSet#next()} method has returned false. The Javadoc + * mentions that implementations are allowed to throw {@link SQLException} in + * this case. In this test we verify that we do not perform any more calls on + * the {@link ResultSet} instance after the result set is closed when the end + * was reached. + */ + @Test + public void shouldNotCallNextWhenResultSetIsClosedForNestedMapping() throws Exception { + + final MappedStatement ms = getNestedAndOrderedMappedStatement(); + final ResultMap rm = ms.getResultMaps().get(0); + + final Executor executor = null; + final ParameterHandler parameterHandler = null; + final TestResultHandler resultHandler = new TestResultHandler(); + final BoundSql boundSql = null; + final RowBounds rowBounds = RowBounds.DEFAULT; + + final DefaultResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, ms, parameterHandler, resultHandler, boundSql, rowBounds); + + when(stmt.getResultSet()).thenReturn(rs); + when(rs.getMetaData()).thenReturn(rsmd); + when(rs.getType()).thenReturn(ResultSet.TYPE_FORWARD_ONLY); + //Simulate a JDBC driver that throws an SQL exception when the end of the result set was reached + when(rs.next()).thenReturn(true).thenReturn(false).thenThrow(new SQLException("Not allowed to call next() after false was returned from previous invocation!")); + when(rs.getInt("CoLuMn1")).thenReturn(100); + when(rs.getInt("CoLuMn2")).thenReturn(200); + when(rs.wasNull()).thenReturn(false); + when(rsmd.getColumnCount()).thenReturn(2); + when(rsmd.getColumnLabel(1)).thenReturn("CoLuMn1"); + when(rsmd.getColumnType(1)).thenReturn(Types.INTEGER); + when(rsmd.getColumnClassName(1)).thenReturn(Integer.class.getCanonicalName()); + when(rsmd.getColumnLabel(2)).thenReturn("CoLuMn2"); + when(rsmd.getColumnType(2)).thenReturn(Types.INTEGER); + when(rsmd.getColumnClassName(2)).thenReturn(Integer.class.getCanonicalName()); + when(stmt.getConnection()).thenReturn(conn); + when(conn.getMetaData()).thenReturn(dbmd); + when(dbmd.supportsMultipleResultSets()).thenReturn(false); // for simplicity. + + final ResultSetWrapper rsw = new ResultSetWrapper(rs, ms.getConfiguration()); + + //Read the first and only record + resultSetHandler.handleRowValues(rsw, rm, resultHandler, rowBounds, null); + assertEquals(1, resultHandler.getResults().size()); + + when(rs.isClosed()).thenReturn(true); + when(rs.getType()).thenThrow(new SQLException("No interaction allowed when the result set is closed!")); + + //Call handleRowValues a second time like the DefaultCursor does to make sure no more results are present + resultSetHandler.handleRowValues(rsw, rm, resultHandler, rowBounds, null); + assertEquals(1, resultHandler.getResults().size()); + + assertEquals(100, ((HashMap) resultHandler.getResults().get(0)).get("cOlUmN1")); + assertEquals(200, ((HashMap) ((HashMap) resultHandler.getResults().get(0)).get("nestedData")).get("cOlUmN2")); + } + + private static class TestResultHandler implements ResultHandler { + private final List results = new ArrayList<>(); + + @Override + public void handleResult(ResultContext resultContext) { + results.add(resultContext.getResultObject()); + resultContext.stop(); + } + + public List getResults() { + return results; + } + } /** * Contrary to the spec, some drivers require case-sensitive column names when getting result. @@ -143,5 +221,31 @@ MappedStatement getMappedStatement() { } }).build(); } + + MappedStatement getNestedAndOrderedMappedStatement() { + final Configuration config = new Configuration(); + final TypeHandlerRegistry registry = config.getTypeHandlerRegistry(); + + ResultMap nestedResultMap = new ResultMap.Builder(config, "nestedTestMap", HashMap.class, new ArrayList() { + { + add(new ResultMapping.Builder(config, "cOlUmN2", "CoLuMn2", registry.getTypeHandler(Integer.class)).build()); + } + }).build(); + config.addResultMap(nestedResultMap); + + return new MappedStatement.Builder(config, "testSelect", new StaticSqlSource(config, "some select statement"), SqlCommandType.SELECT).resultMaps( + new ArrayList() { + { + add(new ResultMap.Builder(config, "testMap", HashMap.class, new ArrayList() { + { + add(new ResultMapping.Builder(config, "cOlUmN1", "CoLuMn1", registry.getTypeHandler(Integer.class)).build()); + add(new ResultMapping.Builder(config, "nestedData").nestedResultMapId("nestedTestMap").build()); + } + }).build()); + } + }) + .resultOrdered(true) + .build(); + } }