diff --git a/pom.xml b/pom.xml index 22f2659de26..9436144e895 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ mybatis - 3.3.1-SNAPSHOT + 3.4.0-SNAPSHOT jar mybatis diff --git a/src/main/java/org/apache/ibatis/binding/MapperMethod.java b/src/main/java/org/apache/ibatis/binding/MapperMethod.java index 7176cc357be..69526229260 100644 --- a/src/main/java/org/apache/ibatis/binding/MapperMethod.java +++ b/src/main/java/org/apache/ibatis/binding/MapperMethod.java @@ -18,6 +18,7 @@ import org.apache.ibatis.annotations.Flush; import org.apache.ibatis.annotations.MapKey; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.reflection.MetaObject; @@ -64,6 +65,8 @@ public Object execute(SqlSession sqlSession, Object[] args) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); + } else if (method.returnsCursor()) { + result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); @@ -132,6 +135,18 @@ private Object executeForMany(SqlSession sqlSession, Object[] args) { return result; } + private Cursor executeForCursor(SqlSession sqlSession, Object[] args) { + Cursor result; + Object param = method.convertArgsToSqlCommandParam(args); + if (method.hasRowBounds()) { + RowBounds rowBounds = method.extractRowBounds(args); + result = sqlSession.selectCursor(command.getName(), param, rowBounds); + } else { + result = sqlSession.selectCursor(command.getName(), param); + } + return result; + } + private Object convertToDeclaredCollection(Configuration config, List list) { Object collection = config.getObjectFactory().create(method.getReturnType()); MetaObject metaObject = config.newMetaObject(collection); @@ -218,6 +233,7 @@ public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; + private final boolean returnsCursor; private final Class returnType; private final String mapKey; private final Integer resultHandlerIndex; @@ -229,6 +245,7 @@ public MethodSignature(Configuration configuration, Method method) { this.returnType = method.getReturnType(); this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray()); + this.returnsCursor = Cursor.class.equals(this.returnType); this.mapKey = getMapKey(method); this.returnsMap = (this.mapKey != null); this.hasNamedParameters = hasNamedParams(method); @@ -295,6 +312,10 @@ public boolean returnsVoid() { return returnsVoid; } + public boolean returnsCursor() { + return returnsCursor; + } + private Integer getUniqueParamIndex(Method method, Class paramType) { Integer index = null; final Class[] argTypes = method.getParameterTypes(); diff --git a/src/main/java/org/apache/ibatis/cursor/Cursor.java b/src/main/java/org/apache/ibatis/cursor/Cursor.java new file mode 100644 index 00000000000..49f8814ca14 --- /dev/null +++ b/src/main/java/org/apache/ibatis/cursor/Cursor.java @@ -0,0 +1,45 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.cursor; + +import java.io.Closeable; + +/** + * Cursor contract to handle fetching items lazily using an Iterator. + * Cursors are a perfect fit to handle millions of items queries that would not normally fits in memory. + * Cursor SQL queries must be ordered (resultOrdered="true") using the id columns of the resultMap. + * + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public interface Cursor extends Closeable, Iterable { + + /** + * @return true if the cursor has started to fetch items from database. + */ + boolean isOpen(); + + /** + * + * @return true if the cursor is fully consumed and has returned all elements matching the query. + */ + boolean isConsumed(); + + /** + * Get the current item index. The first item has the index 0. + * @return -1 if the cursor is not open and has not been consumed. The index of the current item retrieved. + */ + int getCurrentIndex(); +} diff --git a/src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java b/src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java new file mode 100644 index 00000000000..30c86afd7bb --- /dev/null +++ b/src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java @@ -0,0 +1,189 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.cursor.defaults; + +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.executor.resultset.DefaultResultSetHandler; +import org.apache.ibatis.executor.resultset.ResultSetWrapper; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.session.ResultContext; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public class DefaultCursor implements Cursor { + + // ResultSetHandler stuff + private final DefaultResultSetHandler resultSetHandler; + private final ResultMap resultMap; + private final ResultSetWrapper rsw; + private final RowBounds rowBounds; + private final ObjectWrapperResultHandler objectWrapperResultHandler = new ObjectWrapperResultHandler(); + + private int currentIndex = -1; + + private boolean iteratorAlreadyOpened = false; + + private boolean opened = false; + + private boolean resultSetConsumed = false; + + public DefaultCursor(DefaultResultSetHandler resultSetHandler, ResultMap resultMap, ResultSetWrapper rsw, RowBounds rowBounds) { + this.resultSetHandler = resultSetHandler; + this.resultMap = resultMap; + this.rsw = rsw; + this.rowBounds = rowBounds; + } + + @Override + public boolean isOpen() { + return opened; + } + + @Override + public boolean isConsumed() { + return resultSetConsumed; + } + + @Override + public int getCurrentIndex() { + return currentIndex; + } + + @Override + public Iterator iterator() { + return new CursorIterator(); + } + + @Override + public void close() { + ResultSet rs = rsw.getResultSet(); + try { + if (rs != null) { + Statement statement = rs.getStatement(); + + rs.close(); + if (statement != null) { + statement.close(); + } + } + opened = false; + } catch (SQLException e) { + // ignore + } + } + + protected T fetchNextUsingRowBound() { + T result = fetchNextObjectFromDatabase(); + while (currentIndex < rowBounds.getOffset()) { + result = fetchNextObjectFromDatabase(); + } + return result; + } + + protected T fetchNextObjectFromDatabase() { + if (resultSetConsumed) { + return null; + } + + try { + opened = true; + resultSetHandler.handleRowValues(rsw, resultMap, objectWrapperResultHandler, RowBounds.DEFAULT, null); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + T next = objectWrapperResultHandler.result; + if (next != null) { + currentIndex++; + } + // No more object or limit reached + if (next == null || (getReadItemsCount() == rowBounds.getOffset() + rowBounds.getLimit())) { + close(); + resultSetConsumed = true; + } + objectWrapperResultHandler.result = null; + + return next; + } + + private int getReadItemsCount() { + return currentIndex + 1; + } + + private static class ObjectWrapperResultHandler implements ResultHandler { + + private E result; + + @Override + public void handleResult(ResultContext context) { + this.result = (E) context.getResultObject(); + context.stop(); + } + } + + private class CursorIterator implements Iterator { + + /** + * Holder for the next objet to be returned + */ + T object; + + public CursorIterator() { + if (iteratorAlreadyOpened) { + throw new IllegalStateException("Cannot open more than one iterator on a Cursor"); + } + iteratorAlreadyOpened = true; + } + + @Override + public boolean hasNext() { + if (object == null) { + object = fetchNextUsingRowBound(); + } + return object != null; + } + + @Override + public T next() { + // Fill next with object fetched from hasNext() + T next = object; + + if (next == null) { + next = fetchNextUsingRowBound(); + } + + if (next != null) { + object = null; + return next; + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot currently remove element from Cursor"); + } + } +} diff --git a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java index 18acfbd238e..96e40d54d9b 100644 --- a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java @@ -25,6 +25,8 @@ import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.cache.impl.PerpetualCache; +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.jdbc.SQL; import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.logging.jdbc.ConnectionLogger; @@ -170,6 +172,12 @@ public List query(MappedStatement ms, Object parameter, RowBounds rowBoun return list; } + @Override + public Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException { + BoundSql boundSql = ms.getBoundSql(parameter); + return doQueryCursor(ms, parameter, rowBounds, boundSql); + } + @Override public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class targetType) { if (closed) { @@ -219,7 +227,7 @@ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBo cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; - } + } @Override public boolean isCached(MappedStatement ms, CacheKey key) { @@ -269,6 +277,9 @@ protected abstract List doFlushStatements(boolean isRollback) protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; + protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) + throws SQLException; + protected void closeStatement(Statement statement) { if (statement != null) { try { diff --git a/src/main/java/org/apache/ibatis/executor/BatchExecutor.java b/src/main/java/org/apache/ibatis/executor/BatchExecutor.java index 14b0585ebe1..237d1e43b92 100644 --- a/src/main/java/org/apache/ibatis/executor/BatchExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/BatchExecutor.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; import org.apache.ibatis.executor.keygen.KeyGenerator; import org.apache.ibatis.executor.keygen.NoKeyGenerator; @@ -94,6 +95,17 @@ public List doQuery(MappedStatement ms, Object parameterObject, RowBounds } } + @Override + protected Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { + flushStatements(); + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql); + Connection connection = getConnection(ms.getStatementLog()); + Statement stmt = handler.prepare(connection); + handler.parameterize(stmt); + return handler.queryCursor(stmt); + } + @Override public List doFlushStatements(boolean isRollback) throws SQLException { try { diff --git a/src/main/java/org/apache/ibatis/executor/CachingExecutor.java b/src/main/java/org/apache/ibatis/executor/CachingExecutor.java index e8b39c2adb3..f19288082fe 100644 --- a/src/main/java/org/apache/ibatis/executor/CachingExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/CachingExecutor.java @@ -21,6 +21,7 @@ import org.apache.ibatis.cache.Cache; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.cache.TransactionalCacheManager; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; @@ -82,6 +83,12 @@ public List query(MappedStatement ms, Object parameterObject, RowBounds r return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } + @Override + public Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException { + flushCacheIfRequired(ms); + return delegate.queryCursor(ms, parameter, rowBounds); + } + @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { diff --git a/src/main/java/org/apache/ibatis/executor/Executor.java b/src/main/java/org/apache/ibatis/executor/Executor.java index ec51ff17a55..5228e184f9a 100644 --- a/src/main/java/org/apache/ibatis/executor/Executor.java +++ b/src/main/java/org/apache/ibatis/executor/Executor.java @@ -19,6 +19,7 @@ import java.util.List; import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.reflection.MetaObject; @@ -39,6 +40,8 @@ public interface Executor { List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; + Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; + List flushStatements() throws SQLException; void commit(boolean required) throws SQLException; diff --git a/src/main/java/org/apache/ibatis/executor/ReuseExecutor.java b/src/main/java/org/apache/ibatis/executor/ReuseExecutor.java index 20a9a808da1..ca81090601b 100644 --- a/src/main/java/org/apache/ibatis/executor/ReuseExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/ReuseExecutor.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.logging.Log; import org.apache.ibatis.mapping.BoundSql; @@ -59,6 +60,14 @@ public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBo return handler.query(stmt, resultHandler); } + @Override + protected Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql); + Statement stmt = prepareStatement(handler, ms.getStatementLog()); + return handler.queryCursor(stmt); + } + @Override public List doFlushStatements(boolean isRollback) throws SQLException { for (Statement stmt : statementMap.values()) { diff --git a/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java b/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java index 9495056a205..f57877e53bd 100644 --- a/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java @@ -15,12 +15,7 @@ */ package org.apache.ibatis.executor; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Collections; -import java.util.List; - +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.logging.Log; import org.apache.ibatis.mapping.BoundSql; @@ -30,6 +25,12 @@ import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.transaction.Transaction; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; +import java.util.List; + /** * @author Clinton Begin */ @@ -65,6 +66,14 @@ public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBo } } + @Override + protected Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql); + Statement stmt = prepareStatement(handler, ms.getStatementLog()); + return handler.queryCursor(stmt); + } + @Override public List doFlushStatements(boolean isRollback) throws SQLException { return Collections.emptyList(); diff --git a/src/main/java/org/apache/ibatis/executor/loader/ResultLoaderMap.java b/src/main/java/org/apache/ibatis/executor/loader/ResultLoaderMap.java index 7d1777c6cca..c8811c16427 100644 --- a/src/main/java/org/apache/ibatis/executor/loader/ResultLoaderMap.java +++ b/src/main/java/org/apache/ibatis/executor/loader/ResultLoaderMap.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Set; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.BaseExecutor; import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.executor.ExecutorException; @@ -300,5 +301,10 @@ protected List doFlushStatements(boolean isRollback) throws SQLExce protected List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { throw new UnsupportedOperationException("Not supported."); } + + @Override + protected Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { + throw new UnsupportedOperationException("Not supported."); + } } } 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 a025097cf43..afcb1ff8394 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -15,20 +15,9 @@ */ package org.apache.ibatis.executor.resultset; -import java.lang.reflect.Constructor; -import java.sql.CallableStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.cursor.defaults.DefaultCursor; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.ExecutorException; @@ -57,6 +46,19 @@ import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; +import java.lang.reflect.Constructor; +import java.sql.CallableStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + /** * @author Clinton Begin * @author Eduardo Macarron @@ -80,6 +82,7 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final Map nestedResultObjects = new HashMap(); private final Map ancestorObjects = new HashMap(); private final Map ancestorColumnPrefix = new HashMap(); + private Object previousRowValue; // multiple resultsets private final Map nextResultMaps = new HashMap(); @@ -181,6 +184,24 @@ public List handleResultSets(Statement stmt) throws SQLException { return collapseSingleResultList(multipleResults); } + @Override + public Cursor handleCursorResultSets(Statement stmt) throws SQLException { + ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId()); + + ResultSetWrapper rsw = getFirstResultSet(stmt); + + List 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); + return new DefaultCursor(this, resultMap, rsw, rowBounds); + } + private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException { ResultSet rs = stmt.getResultSet(); while (rs == null) { @@ -264,7 +285,7 @@ private List collapseSingleResultList(List multipleResults) { // HANDLE ROWS FOR SIMPLE RESULTMAP // - private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { + public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if (resultMap.hasNestedResultMaps()) { ensureNoRowBounds(); checkResultHandler(); @@ -741,7 +762,7 @@ 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); - Object rowValue = null; + 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); @@ -762,6 +783,9 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r } if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) { storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); + previousRowValue = null; + } else if (rowValue != null) { + previousRowValue = rowValue; } } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/ResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/ResultSetHandler.java index 63ab7846458..e6fe8c85522 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/ResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/ResultSetHandler.java @@ -15,6 +15,8 @@ */ package org.apache.ibatis.executor.resultset; +import org.apache.ibatis.cursor.Cursor; + import java.sql.CallableStatement; import java.sql.SQLException; import java.sql.Statement; @@ -27,6 +29,8 @@ public interface ResultSetHandler { List handleResultSets(Statement stmt) throws SQLException; + Cursor handleCursorResultSets(Statement stmt) throws SQLException; + void handleOutputParameters(CallableStatement cs) throws SQLException; } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java b/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java index 67553d1d375..e3d05f84231 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java @@ -38,7 +38,7 @@ /** * @author Iwao AVE! */ -class ResultSetWrapper { +public class ResultSetWrapper { private final ResultSet resultSet; private final TypeHandlerRegistry typeHandlerRegistry; diff --git a/src/main/java/org/apache/ibatis/executor/statement/CallableStatementHandler.java b/src/main/java/org/apache/ibatis/executor/statement/CallableStatementHandler.java index dbe91f92103..5a70aada5ff 100644 --- a/src/main/java/org/apache/ibatis/executor/statement/CallableStatementHandler.java +++ b/src/main/java/org/apache/ibatis/executor/statement/CallableStatementHandler.java @@ -22,6 +22,7 @@ import java.sql.Statement; import java.util.List; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.ExecutorException; import org.apache.ibatis.executor.keygen.KeyGenerator; @@ -69,6 +70,15 @@ public List query(Statement statement, ResultHandler resultHandler) throw return resultList; } + @Override + public Cursor queryCursor(Statement statement) throws SQLException { + CallableStatement cs = (CallableStatement) statement; + cs.execute(); + Cursor resultList = resultSetHandler.handleCursorResultSets(cs); + resultSetHandler.handleOutputParameters(cs); + return resultList; + } + @Override protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); diff --git a/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java b/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java index 2aa09de59ce..de3e29bd9db 100644 --- a/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java +++ b/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java @@ -22,6 +22,7 @@ import java.sql.Statement; import java.util.List; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; import org.apache.ibatis.executor.keygen.KeyGenerator; @@ -63,6 +64,13 @@ public List query(Statement statement, ResultHandler resultHandler) throw return resultSetHandler. handleResultSets(ps); } + @Override + public Cursor 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(); diff --git a/src/main/java/org/apache/ibatis/executor/statement/RoutingStatementHandler.java b/src/main/java/org/apache/ibatis/executor/statement/RoutingStatementHandler.java index 065a1f7f3a2..5731865eb9e 100644 --- a/src/main/java/org/apache/ibatis/executor/statement/RoutingStatementHandler.java +++ b/src/main/java/org/apache/ibatis/executor/statement/RoutingStatementHandler.java @@ -20,6 +20,7 @@ import java.sql.Statement; import java.util.List; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.ExecutorException; import org.apache.ibatis.executor.parameter.ParameterHandler; @@ -78,6 +79,11 @@ public List query(Statement statement, ResultHandler resultHandler) throw return delegate.query(statement, resultHandler); } + @Override + public Cursor queryCursor(Statement statement) throws SQLException { + return delegate.queryCursor(statement); + } + @Override public BoundSql getBoundSql() { return delegate.getBoundSql(); diff --git a/src/main/java/org/apache/ibatis/executor/statement/SimpleStatementHandler.java b/src/main/java/org/apache/ibatis/executor/statement/SimpleStatementHandler.java index 4e72227b42c..ccbd3e17e45 100644 --- a/src/main/java/org/apache/ibatis/executor/statement/SimpleStatementHandler.java +++ b/src/main/java/org/apache/ibatis/executor/statement/SimpleStatementHandler.java @@ -16,11 +16,13 @@ package org.apache.ibatis.executor.statement; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.List; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; import org.apache.ibatis.executor.keygen.KeyGenerator; @@ -73,6 +75,13 @@ public List query(Statement statement, ResultHandler resultHandler) throw return resultSetHandler.handleResultSets(statement); } + @Override + public Cursor queryCursor(Statement statement) throws SQLException { + String sql = boundSql.getSql(); + statement.execute(sql); + return resultSetHandler.handleCursorResultSets(statement); + } + @Override protected Statement instantiateStatement(Connection connection) throws SQLException { if (mappedStatement.getResultSetType() != null) { diff --git a/src/main/java/org/apache/ibatis/executor/statement/StatementHandler.java b/src/main/java/org/apache/ibatis/executor/statement/StatementHandler.java index b47fe6127dd..e02b4254dcb 100644 --- a/src/main/java/org/apache/ibatis/executor/statement/StatementHandler.java +++ b/src/main/java/org/apache/ibatis/executor/statement/StatementHandler.java @@ -20,6 +20,7 @@ import java.sql.Statement; import java.util.List; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.session.ResultHandler; @@ -44,6 +45,9 @@ int update(Statement statement) List query(Statement statement, ResultHandler resultHandler) throws SQLException; + Cursor queryCursor(Statement statement) + throws SQLException; + BoundSql getBoundSql(); ParameterHandler getParameterHandler(); diff --git a/src/main/java/org/apache/ibatis/session/SqlSession.java b/src/main/java/org/apache/ibatis/session/SqlSession.java index 07d6ef195bf..dba979584d9 100644 --- a/src/main/java/org/apache/ibatis/session/SqlSession.java +++ b/src/main/java/org/apache/ibatis/session/SqlSession.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.BatchResult; /** @@ -115,6 +116,33 @@ public interface SqlSession extends Closeable { */ Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds); + /** + * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator. + * @param the returned cursor element type. + * @param statement Unique identifier matching the statement to use. + * @return Cursor of mapped objects + */ + Cursor selectCursor(String statement); + + /** + * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator. + * @param the returned cursor element type. + * @param statement Unique identifier matching the statement to use. + * @param parameter A parameter object to pass to the statement. + * @return Cursor of mapped objects + */ + Cursor selectCursor(String statement, Object parameter); + + /** + * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator. + * @param the returned cursor element type. + * @param statement Unique identifier matching the statement to use. + * @param parameter A parameter object to pass to the statement. + * @param rowBounds Bounds to limit object retrieval + * @return Cursor of mapped objects + */ + Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds); + /** * Retrieve a single row mapped from the statement key and parameter * using a {@code ResultHandler}. diff --git a/src/main/java/org/apache/ibatis/session/SqlSessionManager.java b/src/main/java/org/apache/ibatis/session/SqlSessionManager.java index ed9d486bde6..d33e21d614d 100644 --- a/src/main/java/org/apache/ibatis/session/SqlSessionManager.java +++ b/src/main/java/org/apache/ibatis/session/SqlSessionManager.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Properties; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.reflection.ExceptionUtil; @@ -180,6 +181,21 @@ public Map selectMap(String statement, Object parameter, String map return sqlSessionProxy. selectMap(statement, parameter, mapKey, rowBounds); } + @Override + public Cursor selectCursor(String statement) { + return sqlSessionProxy.selectCursor(statement); + } + + @Override + public Cursor selectCursor(String statement, Object parameter) { + return sqlSessionProxy.selectCursor(statement, parameter); + } + + @Override + public Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds) { + return sqlSessionProxy.selectCursor(statement, parameter, rowBounds); + } + @Override public List selectList(String statement) { return sqlSessionProxy. selectList(statement); diff --git a/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java b/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java index 6b3e16bf59e..55ceec14090 100644 --- a/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java +++ b/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java @@ -15,14 +15,17 @@ */ package org.apache.ibatis.session.defaults; +import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.ibatis.binding.BindingException; +import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.exceptions.ExceptionFactory; import org.apache.ibatis.exceptions.TooManyResultsException; import org.apache.ibatis.executor.BatchResult; @@ -50,6 +53,7 @@ public class DefaultSqlSession implements SqlSession { private boolean autoCommit; private boolean dirty; + private List> cursorList; public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; @@ -103,6 +107,30 @@ public Map selectMap(String statement, Object parameter, String map return mapResultHandler.getMappedResults(); } + @Override + public Cursor selectCursor(String statement) { + return selectCursor(statement, null); + } + + @Override + public Cursor selectCursor(String statement, Object parameter) { + return selectCursor(statement, parameter, RowBounds.DEFAULT); + } + + @Override + public Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + Cursor cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds); + registerCursor(cursor); + return cursor; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + @Override public List selectList(String statement) { return this.selectList(statement, null); @@ -234,12 +262,26 @@ public List flushStatements() { public void close() { try { executor.close(isCommitOrRollbackRequired(false)); + closeCursors(); dirty = false; } finally { ErrorContext.instance().reset(); } } + private void closeCursors() { + if (cursorList != null && cursorList.size() != 0) { + for (Cursor cursor : cursorList) { + try { + cursor.close(); + } catch (IOException e) { + throw ExceptionFactory.wrapException("Error closing cursor. Cause: " + e, e); + } + } + cursorList.clear(); + } + } + @Override public Configuration getConfiguration() { return configuration; @@ -264,6 +306,13 @@ public void clearCache() { executor.clearLocalCache(); } + private void registerCursor(Cursor cursor) { + if (cursorList == null) { + cursorList = new ArrayList>(); + } + cursorList.add(cursor); + } + private boolean isCommitOrRollbackRequired(boolean force) { return (!autoCommit && dirty) || force; } diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_nested/CreateDB.sql b/src/test/java/org/apache/ibatis/submitted/cursor_nested/CreateDB.sql new file mode 100644 index 00000000000..ecf8f675cf9 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_nested/CreateDB.sql @@ -0,0 +1,41 @@ + +-- +-- Copyright 2009-2012 The MyBatis Team +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table users if exists; + +create table users ( + id int, + name varchar(20), + group_id int, + rol_id int +); + +insert into users values(1, 'User1', 1, 1); +insert into users values(1, 'User1', 1, 2); +insert into users values(1, 'User1', 2, 1); +insert into users values(1, 'User1', 2, 2); +insert into users values(1, 'User1', 2, 3); +insert into users values(2, 'User2', 1, 1); +insert into users values(2, 'User2', 1, 2); +insert into users values(2, 'User2', 1, 3); +insert into users values(3, 'User3', 1, 1); +insert into users values(3, 'User3', 2, 1); +insert into users values(3, 'User3', 3, 1); +insert into users values(4, 'User4', 1, 1); +insert into users values(4, 'User4', 1, 2); +insert into users values(4, 'User4', 2, 1); +insert into users values(4, 'User4', 2, 2); \ No newline at end of file diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_nested/CursorNestedTest.java b/src/test/java/org/apache/ibatis/submitted/cursor_nested/CursorNestedTest.java new file mode 100644 index 00000000000..5b4f8410996 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_nested/CursorNestedTest.java @@ -0,0 +1,117 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.cursor_nested; + +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.Reader; +import java.sql.Connection; +import java.util.Iterator; + +public class CursorNestedTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeClass + public static void setUp() throws Exception { + // create a SqlSessionFactory + Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursor_nested/mybatis-config.xml"); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + reader.close(); + + // populate in-memory database + SqlSession session = sqlSessionFactory.openSession(); + Connection conn = session.getConnection(); + reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursor_nested/CreateDB.sql"); + ScriptRunner runner = new ScriptRunner(conn); + runner.setLogWriter(null); + runner.runScript(reader); + reader.close(); + session.close(); + } + + @Test + public void shouldGetAllUser() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + Mapper mapper = sqlSession.getMapper(Mapper.class); + Cursor usersCursor = mapper.getAllUsers(); + + try { + Assert.assertFalse(usersCursor.isOpen()); + // Retrieving iterator, fetching is not started + Iterator iterator = usersCursor.iterator(); + + // Check if hasNext, fetching is started + Assert.assertTrue(iterator.hasNext()); + Assert.assertTrue(usersCursor.isOpen()); + Assert.assertFalse(usersCursor.isConsumed()); + + User user = iterator.next(); + Assert.assertEquals(2, user.getGroups().size()); + Assert.assertEquals(3, user.getRoles().size()); + + user = iterator.next(); + Assert.assertEquals(1, user.getGroups().size()); + Assert.assertEquals(3, user.getRoles().size()); + + user = iterator.next(); + Assert.assertEquals(3, user.getGroups().size()); + Assert.assertEquals(1, user.getRoles().size()); + + user = iterator.next(); + Assert.assertEquals(2, user.getGroups().size()); + Assert.assertEquals(2, user.getRoles().size()); + + // Check no more elements + Assert.assertFalse(iterator.hasNext()); + Assert.assertFalse(usersCursor.isOpen()); + Assert.assertTrue(usersCursor.isConsumed()); + } finally { + sqlSession.close(); + } + Assert.assertFalse(usersCursor.isOpen()); + } + + @Test + public void testCursorWithRowBound() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + + try { + Cursor usersCursor = sqlSession.selectCursor("getAllUsers", null, new RowBounds(2, 1)); + + Iterator iterator = usersCursor.iterator(); + + User user = iterator.next(); + Assert.assertEquals("User3", user.getName()); + Assert.assertEquals(2, usersCursor.getCurrentIndex()); + + Assert.assertFalse(iterator.hasNext()); + Assert.assertFalse(usersCursor.isOpen()); + Assert.assertTrue(usersCursor.isConsumed()); + } finally { + sqlSession.close(); + } + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_nested/Mapper.java b/src/test/java/org/apache/ibatis/submitted/cursor_nested/Mapper.java new file mode 100644 index 00000000000..7be71969b4e --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_nested/Mapper.java @@ -0,0 +1,26 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.cursor_nested; + +import org.apache.ibatis.cursor.Cursor; + +import java.util.List; + +public interface Mapper { + + Cursor getAllUsers(); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_nested/Mapper.xml b/src/test/java/org/apache/ibatis/submitted/cursor_nested/Mapper.xml new file mode 100644 index 00000000000..0e0e35d2d44 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_nested/Mapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_nested/User.java b/src/test/java/org/apache/ibatis/submitted/cursor_nested/User.java new file mode 100644 index 00000000000..ac8762a5465 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_nested/User.java @@ -0,0 +1,68 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.cursor_nested; + +import java.util.List; + +public class User { + + private Integer id; + private String name; + private List groups; + private List roles; + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + ", groups=" + groups + + ", roles=" + roles + + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_nested/mybatis-config.xml b/src/test/java/org/apache/ibatis/submitted/cursor_nested/mybatis-config.xml new file mode 100644 index 00000000000..910919484f4 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_nested/mybatis-config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_simple/CreateDB.sql b/src/test/java/org/apache/ibatis/submitted/cursor_simple/CreateDB.sql new file mode 100644 index 00000000000..a2597dde3e6 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_simple/CreateDB.sql @@ -0,0 +1,27 @@ +-- +-- Copyright 2009-2012 The MyBatis Team +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table users if exists; + +create table users ( + id int, + name varchar(20) +); + +insert into users values(1, 'User1'); +insert into users values(2, 'User2'); +insert into users values(3, 'User3'); +insert into users values(4, 'User4'); diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_simple/CursorSimpleTest.java b/src/test/java/org/apache/ibatis/submitted/cursor_simple/CursorSimpleTest.java new file mode 100644 index 00000000000..12d666c839b --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_simple/CursorSimpleTest.java @@ -0,0 +1,151 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.cursor_simple; + +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.Reader; +import java.sql.Connection; +import java.util.Iterator; + +public class CursorSimpleTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeClass + public static void setUp() throws Exception { + // create a SqlSessionFactory + Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursor_simple/mybatis-config.xml"); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + reader.close(); + + // populate in-memory database + SqlSession session = sqlSessionFactory.openSession(); + Connection conn = session.getConnection(); + reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursor_simple/CreateDB.sql"); + ScriptRunner runner = new ScriptRunner(conn); + runner.setLogWriter(null); + runner.runScript(reader); + reader.close(); + session.close(); + } + + @Test + public void shouldGetAllUser() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + Mapper mapper = sqlSession.getMapper(Mapper.class); + Cursor usersCursor = mapper.getAllUsers(); + try { + Assert.assertFalse(usersCursor.isOpen()); + + Iterator iterator = usersCursor.iterator(); + + // Check if hasNext, fetching is started + Assert.assertTrue(iterator.hasNext()); + Assert.assertTrue(usersCursor.isOpen()); + Assert.assertFalse(usersCursor.isConsumed()); + + User user = iterator.next(); + Assert.assertEquals("User1", user.getName()); + Assert.assertEquals(0, usersCursor.getCurrentIndex()); + + user = iterator.next(); + Assert.assertEquals("User2", user.getName()); + Assert.assertEquals(1, usersCursor.getCurrentIndex()); + + user = iterator.next(); + Assert.assertEquals("User3", user.getName()); + Assert.assertEquals(2, usersCursor.getCurrentIndex()); + + user = iterator.next(); + Assert.assertEquals("User4", user.getName()); + Assert.assertEquals(3, usersCursor.getCurrentIndex()); + + // Check no more elements + Assert.assertFalse(iterator.hasNext()); + Assert.assertFalse(usersCursor.isOpen()); + Assert.assertTrue(usersCursor.isConsumed()); + } finally { + sqlSession.close(); + } + } + + @Test + public void testCursorClosedOnSessionClose() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + Mapper mapper = sqlSession.getMapper(Mapper.class); + Cursor usersCursor = mapper.getAllUsers(); + try { + Assert.assertFalse(usersCursor.isOpen()); + + Iterator iterator = usersCursor.iterator(); + + // Check if hasNext, fetching is started + Assert.assertTrue(iterator.hasNext()); + Assert.assertTrue(usersCursor.isOpen()); + Assert.assertFalse(usersCursor.isConsumed()); + + // Consume only the first result + User user = iterator.next(); + Assert.assertEquals("User1", user.getName()); + + // Check there is still remaining elements + Assert.assertTrue(iterator.hasNext()); + Assert.assertTrue(usersCursor.isOpen()); + Assert.assertFalse(usersCursor.isConsumed()); + } finally { + sqlSession.close(); + } + + // The cursor was not fully consumed, but it should be close since we closed the session + Assert.assertFalse(usersCursor.isOpen()); + Assert.assertFalse(usersCursor.isConsumed()); + } + + @Test + public void testCursorWithRowBound() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + + try { + Cursor usersCursor = sqlSession.selectCursor("getAllUsers", null, new RowBounds(1, 2)); + + Iterator iterator = usersCursor.iterator(); + + User user = iterator.next(); + Assert.assertEquals("User2", user.getName()); + Assert.assertEquals(1, usersCursor.getCurrentIndex()); + + user = iterator.next(); + Assert.assertEquals("User3", user.getName()); + Assert.assertEquals(2, usersCursor.getCurrentIndex()); + + Assert.assertFalse(iterator.hasNext()); + Assert.assertFalse(usersCursor.isOpen()); + Assert.assertTrue(usersCursor.isConsumed()); + } finally { + sqlSession.close(); + } + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.java b/src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.java new file mode 100644 index 00000000000..8994d2cb5ea --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.java @@ -0,0 +1,24 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.cursor_simple; + +import org.apache.ibatis.cursor.Cursor; + +public interface Mapper { + + Cursor getAllUsers(); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.xml b/src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.xml new file mode 100644 index 00000000000..47bf2d6ab8e --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_simple/User.java b/src/test/java/org/apache/ibatis/submitted/cursor_simple/User.java new file mode 100644 index 00000000000..16e70c561ff --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_simple/User.java @@ -0,0 +1,46 @@ +/** + * Copyright 2009-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.cursor_simple; + +public class User { + + private Integer id; + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursor_simple/mybatis-config.xml b/src/test/java/org/apache/ibatis/submitted/cursor_simple/mybatis-config.xml new file mode 100644 index 00000000000..8c0c6dabdba --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursor_simple/mybatis-config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + +