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