From 67602786375ba931d8e5e823968042ae0f8f2701 Mon Sep 17 00:00:00 2001 From: Dieter Deforce Date: Wed, 5 Sep 2018 15:58:44 +0200 Subject: [PATCH] Fix an issue when using mybatis cursor on DB2 The DefaultResultSetHandler would interact with the ResultSet after it was closed automatically when we reached the end, this causes SqlExceptions. This commit implements a workaround that checks if the result set is already closed. --- .../resultset/DefaultResultSetHandler.java | 6 +- .../DefaultResultSetHandlerTest.java | 104 ++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) 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(); + } }