diff --git a/src/main/java/org/apache/ibatis/annotations/Options.java b/src/main/java/org/apache/ibatis/annotations/Options.java index a46de300118..ef4e9b37910 100644 --- a/src/main/java/org/apache/ibatis/annotations/Options.java +++ b/src/main/java/org/apache/ibatis/annotations/Options.java @@ -20,6 +20,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.ResultSetType; import org.apache.ibatis.mapping.StatementType; @@ -43,4 +44,6 @@ String keyProperty() default "id"; String keyColumn() default ""; + + FetchType fetchType() default FetchType.DEFAULT; } diff --git a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java index 0b7ef843b0c..98997795d68 100644 --- a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java +++ b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java @@ -32,6 +32,7 @@ import org.apache.ibatis.executor.keygen.KeyGenerator; import org.apache.ibatis.mapping.CacheBuilder; import org.apache.ibatis.mapping.Discriminator; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMap; import org.apache.ibatis.mapping.ParameterMapping; @@ -287,8 +288,9 @@ public MappedStatement addMappedStatement( String keyColumn, String databaseId, LanguageDriver lang, - String resultSets) { - + String resultSets, + FetchType fetchType) { + if (unresolvedCacheRef) throw new IncompleteElementException("Cache-ref not yet resolved"); id = applyCurrentNamespace(id, false); @@ -305,6 +307,7 @@ public MappedStatement addMappedStatement( statementBuilder.lang(lang); statementBuilder.resultOrdered(resultOrdered); statementBuilder.resulSets(resultSets); + statementBuilder.fetchType(fetchType); setStatementTimeout(timeout, statementBuilder); setStatementParameterMap(parameterMap, parameterType, statementBuilder); diff --git a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java index 325efa55bd6..2f5105091b1 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java @@ -66,6 +66,7 @@ import org.apache.ibatis.executor.keygen.SelectKeyGenerator; import org.apache.ibatis.io.Resources; import org.apache.ibatis.mapping.Discriminator; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultFlag; import org.apache.ibatis.mapping.ResultMapping; @@ -249,6 +250,7 @@ void parseStatement(Method method) { ResultSetType resultSetType = ResultSetType.FORWARD_ONLY; SqlCommandType sqlCommandType = getSqlCommandType(method); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; + FetchType fetchType = FetchType.DEFAULT; boolean flushCache = !isSelect; boolean useCache = isSelect; @@ -281,6 +283,7 @@ void parseStatement(Method method) { timeout = options.timeout() > -1 ? options.timeout() : null; statementType = options.statementType(); resultSetType = options.resultSetType(); + fetchType = options.fetchType(); } String resultMapId = null; @@ -317,7 +320,8 @@ void parseStatement(Method method) { keyColumn, null, languageDriver, - null); + null, + fetchType); } } @@ -553,7 +557,7 @@ private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, St assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, - keyGenerator, keyProperty, null, null, languageDriver, null); + keyGenerator, keyProperty, null, null, languageDriver, null, FetchType.DEFAULT); id = assistant.applyCurrentNamespace(id, false); diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java index e9b1ae2d7c7..bfe311a21b2 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java @@ -24,6 +24,7 @@ import org.apache.ibatis.executor.keygen.KeyGenerator; import org.apache.ibatis.executor.keygen.NoKeyGenerator; import org.apache.ibatis.executor.keygen.SelectKeyGenerator; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultSetType; import org.apache.ibatis.mapping.SqlCommandType; @@ -77,6 +78,7 @@ public void parseStatementNode() { boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); + FetchType fetchType = FetchType.valueOf(context.getStringAttribute("fetchType", FetchType.DEFAULT.toString())); // Include Fragments before parsing XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); @@ -109,7 +111,7 @@ public void parseStatementNode() { builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, - keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, fetchType); } public void parseSelectKeyNodes(String parentId, List list, Class parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) { @@ -146,7 +148,7 @@ public void parseSelectKeyNode(String id, XNode nodeToHandle, Class parameter builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, - keyGenerator, keyProperty, null, databaseId, langDriver, null); + keyGenerator, keyProperty, null, databaseId, langDriver, null, FetchType.DEFAULT); id = builderAssistant.applyCurrentNamespace(id, false); diff --git a/src/main/java/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd b/src/main/java/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd index 40661b02383..6cbd3542c95 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd +++ b/src/main/java/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd @@ -169,6 +169,7 @@ resultMap CDATA #IMPLIED resultType CDATA #IMPLIED resultSetType (FORWARD_ONLY | SCROLL_INSENSITIVE | SCROLL_SENSITIVE) #IMPLIED statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED +fetchType (DEFAULT|CURSOR|LAZY) #IMPLIED fetchSize CDATA #IMPLIED timeout CDATA #IMPLIED flushCache (true|false) #IMPLIED diff --git a/src/main/java/org/apache/ibatis/executor/BatchExecutor.java b/src/main/java/org/apache/ibatis/executor/BatchExecutor.java index 01c53db1976..64ec33fd77a 100644 --- a/src/main/java/org/apache/ibatis/executor/BatchExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/BatchExecutor.java @@ -27,6 +27,7 @@ import org.apache.ibatis.executor.keygen.KeyGenerator; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; @@ -82,7 +83,9 @@ public List doQuery(MappedStatement ms, Object parameterObject, RowBounds handler.parameterize(stmt); return handler.query(stmt, resultHandler); } finally { - closeStatement(stmt); + if (ms.getFetchType() == FetchType.DEFAULT) { + closeStatement(stmt); + } } } diff --git a/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java b/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java index 5325a66fc05..e4793060d79 100644 --- a/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java @@ -24,6 +24,7 @@ import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.logging.Log; import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; @@ -56,7 +57,9 @@ public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBo stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { - closeStatement(stmt); + if (ms.getFetchType() == FetchType.DEFAULT) { + closeStatement(stmt); + } } } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/CursorList.java b/src/main/java/org/apache/ibatis/executor/resultset/CursorList.java new file mode 100644 index 00000000000..aef68af1544 --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/CursorList.java @@ -0,0 +1,182 @@ +/* + * Copyright 2009-2013 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.executor.resultset; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.AbstractList; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; +import org.apache.ibatis.session.ResultContext; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public class CursorList extends AbstractList { + + // ResultSetHandler stuff + private final DefaultResultSetHandler resultSetHandler; + private final ResultMap resultMap; + private final ResultSetWrapper rsw; + private final ResultMapping parentMapping; + private final ObjectWrapperResultHandler objectWrapperResultHandler = new ObjectWrapperResultHandler(); + + private boolean iteratorAlreadyOpened = false; + + private boolean fetchStarted = false; + + private boolean resultSetExhausted = false; + + public CursorList(CursorResultSetHandler resultSetHandler, ResultSetWrapper rsw, ResultMap resultMap, ResultMapping parentMapping) { + this.resultSetHandler = resultSetHandler; + this.rsw = rsw; + this.resultMap = resultMap; + this.parentMapping = parentMapping; + } + + @Override + public E get(int index) { + throw new UnsupportedOperationException("Cannot retrieve object at a specific index in CursorList. " + + "Consider using Iterator to browse the list."); + } + + @Override + public int size() { + throw new UnsupportedOperationException("Cannot retrieve size of a CursorList. " + + "Consider using Iterator to browse the list."); + } + + public boolean isResultSetExhausted() { + return resultSetExhausted; + } + + public boolean isFetchStarted() { + return fetchStarted; + } + + @Override + public Iterator iterator() { + return new CursorIterator(); + } + + protected E fetchNextObjectFromDatabase() { + if (resultSetExhausted) return null; + + try { + fetchStarted = true; + resultSetHandler.handleRowValues(rsw, resultMap, objectWrapperResultHandler, RowBounds.DEFAULT, parentMapping); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + E next = objectWrapperResultHandler.result; + if (next == null) { + closeResultSetAndStatement(); + resultSetExhausted = true; + } + objectWrapperResultHandler.result = null; + + return next; + } + + public void closeResultSetAndStatement() { + ResultSet rs = rsw.getResultSet(); + try { + if (rs != null) { + Statement statement = rs.getStatement(); + + rs.close(); + if (statement != null) { + statement.close(); + } + } + } catch (SQLException e) { + // ignore + } + } + + /** + * This toString returns Object's toString default implementation since we don't want AbstractCollection#toString() + * to iterate on collection. + * + * @return a string representation of the object. + */ + @Override + public String toString() { + return getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(this)); + } + + private static class ObjectWrapperResultHandler implements ResultHandler { + + private E result; + + @Override + public void handleResult(ResultContext context) { + this.result = (E) context.getResultObject(); + } + } + + private class CursorIterator implements Iterator { + + /** + * Holder for the next objet to be returned + */ + E object; + + public CursorIterator() { + if (iteratorAlreadyOpened) { + throw new IllegalStateException("Cannot open more than one iterator on a CursorList. " + + "Use LazyList if you want to iterate more than one time."); + } + iteratorAlreadyOpened = true; + } + + @Override + public boolean hasNext() { + if (object == null) { + object = fetchNextObjectFromDatabase(); + } + return object != null; + } + + @Override + public E next() { + // Fill next with object fetched from hasNext() + E next = object; + + if (next == null) { + next = fetchNextObjectFromDatabase(); + } + + if (next != null) { + object = null; + return next; + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot remove element from CursorList"); + } + } +} diff --git a/src/main/java/org/apache/ibatis/executor/resultset/CursorResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/CursorResultSetHandler.java new file mode 100644 index 00000000000..31c9a385854 --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/CursorResultSetHandler.java @@ -0,0 +1,103 @@ +/* + * Copyright 2009-2013 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.executor.resultset; + +import java.sql.SQLException; +import java.util.List; + +import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.parameter.ParameterHandler; +import org.apache.ibatis.executor.result.DefaultResultContext; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.FetchType; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public class CursorResultSetHandler extends DefaultResultSetHandler { + + private final FetchType fetchType; + private Object previousRowValue; + + public CursorResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql, RowBounds rowBounds, FetchType fetchType) { + super(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); + this.fetchType = fetchType; + } + + @Override + protected void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List multipleResults, ResultMapping parentMapping) throws SQLException { + if (resultHandler == null) { + List cursorList = getResultList(rsw, resultMap, parentMapping); + multipleResults.add(cursorList); + } else { + throw new IllegalStateException("CursorNestedResultSetHandler cannot be used with external ResultHandler"); + } + } + + private List getResultList(ResultSetWrapper rsw, ResultMap resultMap, ResultMapping parentMapping) { + if (fetchType == FetchType.CURSOR) { + return new CursorList(this, rsw, resultMap, parentMapping); + } else if (fetchType == FetchType.LAZY) { + return new LazyList(this, rsw, resultMap, parentMapping); + } else { + throw new IllegalArgumentException("FetchType " + fetchType + + " is not supported by CursorNestedResultSetHandler"); + } + } + + @Override + protected 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 = previousRowValue; + while (shouldProcessMoreRows(rsw.getResultSet(), resultContext, rowBounds)) { + final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null); + final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); + Object partialObject = nestedResultObjects.get(rowKey); + if (mappedStatement.isResultOrdered()) { // issue #577 && #542 + if (partialObject == null && rowValue != null) { + nestedResultObjects.clear(); + storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); + } + rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, rowKey, null, + partialObject); + } else { + rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, rowKey, null, + partialObject); + if (partialObject == null) { + storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); + } + } + } + if (rowValue != null && mappedStatement.isResultOrdered() && !resultContext.isStopped()) { + storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); + previousRowValue = null; + } else if (rowValue != null) { + previousRowValue = rowValue; + } + } + + protected void callResultHandler(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue) { + super.callResultHandler(resultHandler, resultContext, rowValue); + resultContext.stop(); + } +} 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 8849cc8721f..ca9aa558d05 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -62,10 +62,10 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final Executor executor; private final Configuration configuration; - private final MappedStatement mappedStatement; + protected final MappedStatement mappedStatement; private final RowBounds rowBounds; private final ParameterHandler parameterHandler; - private final ResultHandler resultHandler; + protected final ResultHandler resultHandler; private final BoundSql boundSql; private final TypeHandlerRegistry typeHandlerRegistry; private final ObjectFactory objectFactory; @@ -73,7 +73,7 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final ResultExtractor resultExtractor; // nested resultmaps - private final Map nestedResultObjects = new HashMap(); + protected final Map nestedResultObjects = new HashMap(); private final Map ancestorObjects = new HashMap(); // multiple resultsets @@ -224,7 +224,7 @@ private void validateResultMapsCount(ResultSetWrapper rsw, int resultMapCount) { } } - private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List multipleResults, ResultMapping parentMapping) throws SQLException { + protected void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List multipleResults, ResultMapping parentMapping) throws SQLException { try { if (parentMapping != null) { handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping); @@ -255,7 +255,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 { + protected void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if (resultMap.hasNestedResultMaps()) { ensureNoRowBounds(); checkResultHandler(); @@ -291,7 +291,7 @@ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap r } } - private void storeObject(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException { + protected void storeObject(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException { if (parentMapping != null) { linkToParent(rs, parentMapping, rowValue); } else { @@ -299,16 +299,16 @@ private void storeObject(ResultHandler resultHandler, DefaultResultContext resul } } - private void callResultHandler(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue) { + protected void callResultHandler(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue) { resultContext.nextResultObject(rowValue); resultHandler.handleResult(resultContext); } - private boolean shouldProcessMoreRows(ResultSet rs, ResultContext context, RowBounds rowBounds) throws SQLException { + protected boolean shouldProcessMoreRows(ResultSet rs, ResultContext context, RowBounds rowBounds) throws SQLException { return !context.isStopped() && rs.next() && context.getResultCount() < rowBounds.getLimit(); } - private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException { + protected void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException { if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) { if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) { rs.absolute(rowBounds.getOffset()); @@ -712,7 +712,7 @@ private String prependPrefix(String columnName, String prefix) { // HANDLE NESTED RESULT MAPS // - private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { + protected 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; @@ -742,7 +742,7 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r // GET VALUE FROM ROW FOR NESTED RESULT MAP // - private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, CacheKey absoluteKey, String columnPrefix, Object partialObject) throws SQLException { + protected Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, CacheKey absoluteKey, String columnPrefix, Object partialObject) throws SQLException { Object resultObject = partialObject; if (resultObject != null) { final MetaObject metaObject = configuration.newMetaObject(resultObject); @@ -846,7 +846,7 @@ private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, Str // UNIQUE RESULT KEY // - private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException { + protected CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException { final CacheKey cacheKey = new CacheKey(); cacheKey.update(resultMap.getId()); List resultMappings = getResultMappingsForRowKey(resultMap); diff --git a/src/main/java/org/apache/ibatis/executor/resultset/LazyList.java b/src/main/java/org/apache/ibatis/executor/resultset/LazyList.java new file mode 100644 index 00000000000..76ee0e2ac20 --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/LazyList.java @@ -0,0 +1,175 @@ +/* + * Copyright 2009-2013 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.executor.resultset; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public class LazyList extends AbstractList { + + private final CursorList cursorList; + + // Storage for already fetched elements + private final ArrayList storage = new ArrayList(); + + /** + * Convenient constructor that takes a List, removing the need for an explicit cast. + * But list implementation must be CursorList. + * + * @param cursorList + * @throws IllegalArgumentException if list is not a CursorList + */ + public LazyList(List cursorList) { + if (!(cursorList instanceof CursorList)) { + throw new IllegalArgumentException("A CursorList is mandatory for LazyList"); + } + this.cursorList = (CursorList) cursorList; + checkForStartedCursor(); + } + + public LazyList(CursorList cursorList) { + this.cursorList = cursorList; + checkForStartedCursor(); + } + + public LazyList(CursorResultSetHandler resultSetHandler, ResultSetWrapper rsw, ResultMap resultMap, ResultMapping parentMapping) { + this(new CursorList(resultSetHandler, rsw, resultMap, parentMapping)); + } + + private void checkForStartedCursor() { + if (cursorList.isFetchStarted()) { + throw new IllegalStateException("Cannot use LazyList on a CursorList with already fetched items. " + + "This would leads to a partial view of the result set."); + } + } + + @Override + public E get(int index) { + return getElementAtIndex(index); + } + + /** + * Retrieve elements from storage if available, or fetch from database. + * + * @param index index of the element to return + * @return element at specified index + * @throws IndexOutOfBoundsException if index is higher than elements count. + */ + public E getElementAtIndex(int index) { + if (index < storage.size()) { + return storage.get(index); + } + + E object = null; + int currentIndex = storage.size() - 1; + while (currentIndex < index) { + object = cursorList.fetchNextObjectFromDatabase(); + currentIndex++; + if (object != null) { + storage.add(object); + } else { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + storage.size()); + } + } + return object; + } + + private void consumeCursor() { + int currentIndex = storage.size(); + try { + while (true) { + getElementAtIndex(currentIndex++); + } + } catch (IndexOutOfBoundsException e) { + // Ignore + } + } + + @Override + public int size() { + consumeCursor(); + return storage.size(); + } + + /** + * This toString returns Object's toString default implementation since we don't want AbstractCollection#toString() + * to iterate on collection. + * + * @return a string representation of the object. + */ + @Override + public String toString() { + return getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(this)); + } + + @Override + public Iterator iterator() { + return new LazyIterator(); + } + + private class LazyIterator implements Iterator { + + /** + * Index of element to be returned by subsequent call to next. + */ + int cursor = 0; + /** + * Holder for the next objet to be returned + */ + E object; + + @Override + public boolean hasNext() { + try { + object = getElementAtIndex(cursor); + } catch (IndexOutOfBoundsException e) { + object = null; + } + return object != null; + } + + @Override + public E next() { + // Fill next with object fetched from hasNext() + E next = object; + + if (next == null) { + next = getElementAtIndex(cursor); + } + + if (next != null) { + object = null; + cursor++; + return next; + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot remove element from lazily loaded list"); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/ibatis/mapping/FetchType.java b/src/main/java/org/apache/ibatis/mapping/FetchType.java new file mode 100644 index 00000000000..b2369bf6d29 --- /dev/null +++ b/src/main/java/org/apache/ibatis/mapping/FetchType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2009-2013 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.mapping; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public enum FetchType { + DEFAULT, CURSOR, LAZY +} diff --git a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java index 2b5fb729ad9..194ecc04a61 100644 --- a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java +++ b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java @@ -53,6 +53,7 @@ public final class MappedStatement { private Log statementLog; private LanguageDriver lang; private String[] resultSets; + private FetchType fetchType; private MappedStatement() { // constructor disabled @@ -168,7 +169,12 @@ public Builder resulSets(String resultSet) { mappedStatement.resultSets = delimitedStringtoArray(resultSet); return this; } - + + public Builder fetchType(FetchType fetchType) { + mappedStatement.fetchType = fetchType; + return this; + } + public MappedStatement build() { assert mappedStatement.configuration != null; assert mappedStatement.id != null; @@ -270,7 +276,11 @@ public LanguageDriver getLang() { public String[] getResulSets() { return resultSets; } - + + public FetchType getFetchType() { + return fetchType; + } + public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List parameterMappings = boundSql.getParameterMappings(); diff --git a/src/main/java/org/apache/ibatis/session/Configuration.java b/src/main/java/org/apache/ibatis/session/Configuration.java index 080dd1e1695..33358417d77 100644 --- a/src/main/java/org/apache/ibatis/session/Configuration.java +++ b/src/main/java/org/apache/ibatis/session/Configuration.java @@ -49,6 +49,7 @@ import org.apache.ibatis.executor.loader.cglib.CglibProxyFactory; import org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory; import org.apache.ibatis.executor.parameter.ParameterHandler; +import org.apache.ibatis.executor.resultset.CursorResultSetHandler; import org.apache.ibatis.executor.resultset.DefaultResultSetHandler; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; @@ -64,6 +65,7 @@ import org.apache.ibatis.logging.stdout.StdOutImpl; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMap; import org.apache.ibatis.mapping.ResultMap; @@ -101,10 +103,10 @@ public class Configuration { protected boolean cacheEnabled = true; protected boolean callSettersOnNulls = false; protected String logPrefix; - protected Class logImpl; + protected Class logImpl; protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION; protected JdbcType jdbcTypeForNull = JdbcType.OTHER; - protected Set lazyLoadTriggerMethods = new HashSet(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" })); + protected Set lazyLoadTriggerMethods = new HashSet(Arrays.asList(new String[]{"equals", "clone", "hashCode", "toString"})); protected Integer defaultStatementTimeout; protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL; @@ -453,12 +455,24 @@ public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Obj } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, - ResultHandler resultHandler, BoundSql boundSql) { - ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); + ResultHandler resultHandler, BoundSql boundSql) { + ResultSetHandler resultSetHandler = createResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } + private ResultSetHandler createResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { + ResultSetHandler resultSetHandler; + FetchType fetchType = mappedStatement.getFetchType(); + if (fetchType == FetchType.CURSOR || fetchType == FetchType.LAZY) { + resultSetHandler = new CursorResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds, + fetchType); + } else { + resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); + } + return resultSetHandler; + } + public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); @@ -791,7 +805,7 @@ public V get(Object key) { } if (value instanceof Ambiguity) { throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name - + " (try using the full name including the namespace, or rename one of the entries)"); + + " (try using the full name including the namespace, or rename one of the entries)"); } return value; } @@ -803,6 +817,7 @@ private String getShortName(String key) { } protected static class Ambiguity { + private String subject; public Ambiguity(String subject) { @@ -814,5 +829,4 @@ public String getSubject() { } } } - } diff --git a/src/test/java/com/ibatis/sqlmap/engine/builder/XmlSqlStatementParser.java b/src/test/java/com/ibatis/sqlmap/engine/builder/XmlSqlStatementParser.java index b75c3db3c83..00b74ef497e 100644 --- a/src/test/java/com/ibatis/sqlmap/engine/builder/XmlSqlStatementParser.java +++ b/src/test/java/com/ibatis/sqlmap/engine/builder/XmlSqlStatementParser.java @@ -52,6 +52,7 @@ public void parseGeneralStatement(XNode context) { String resultSetType = context.getStringAttribute("resultSetType"); String fetchSize = context.getStringAttribute("fetchSize"); String timeout = context.getStringAttribute("timeout"); + FetchType fetchType = FetchType.valueOf(context.getStringAttribute("fetchType", FetchType.DEFAULT.toString())); // 2.x -- String allowRemapping = context.getStringAttribute("remapResults"); if (context.getStringAttribute("xmlResultName") != null) { @@ -146,6 +147,8 @@ public void parseGeneralStatement(XNode context) { builder.timeout(timeoutInt); + builder.fetchType(fetchType); + if (cacheModelName != null) { cacheModelName = mapParser.applyNamespace(cacheModelName); Cache cache = configuration.getCache(cacheModelName); diff --git a/src/test/java/org/apache/ibatis/executor/resultset/CursorListTest.java b/src/test/java/org/apache/ibatis/executor/resultset/CursorListTest.java new file mode 100644 index 00000000000..2b1aae8369a --- /dev/null +++ b/src/test/java/org/apache/ibatis/executor/resultset/CursorListTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2009-2013 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.executor.resultset; + +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.isNotNull; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.apache.ibatis.executor.result.DefaultResultContext; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +@RunWith(MockitoJUnitRunner.class) +public class CursorListTest { + + private final int[] fakeResultList = new int[] { 16, 32, 64 }; + + @Test(expected = UnsupportedOperationException.class) + public void testGet() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + cursorList.get(0); + fail("get() call should not be available"); + } + + @Test(expected = UnsupportedOperationException.class) + public void testSize() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + cursorList.size(); + fail("size() call should not be available"); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorRemove() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + + Iterator iter = cursorList.iterator(); + iter.remove(); + fail("iter.remove() call should not be available"); + } + + @Test + public void testNextWithoutHasNext() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + + Iterator iter = cursorList.iterator(); + assertEquals(Integer.valueOf(16), iter.next()); + assertEquals(Integer.valueOf(32), iter.next()); + assertEquals(Integer.valueOf(64), iter.next()); + } + + @Test + public void ensureToStringDoesnotExhaustResultSet() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + + cursorList.toString(); + assertFalse(cursorList.isResultSetExhausted()); + } + + @Test + public void testCursorFetch() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + assertFalse(cursorList.isFetchStarted()); + assertFalse(cursorList.isResultSetExhausted()); + + Iterator iter = cursorList.iterator(); + + // Fetching 1st item + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(16), iter.next()); + assertTrue(cursorList.isFetchStarted()); + assertFalse(cursorList.isResultSetExhausted()); + + // Fetching 2nd item + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(32), iter.next()); + + // Fetching 3rd item + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(64), iter.next()); + + // Check no more results + assertFalse(iter.hasNext()); + assertTrue(cursorList.isFetchStarted()); + assertTrue(cursorList.isResultSetExhausted()); + } + + @Test(expected = IllegalStateException.class) + public void testMoreThanOneIterator() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + int index = 0; + // foreach statement : opens an iterator + for (Integer i : cursorList) { + assertEquals(Integer.valueOf(fakeResultList[index++]), i); + } + + // foreach statement : opens another iterator + for (Integer i : cursorList) { + fail("I should't have reach this line"); + } + } + + @Test(expected = NoSuchElementException.class) + public void testNoSuchElementException() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + + Iterator iter = cursorList.iterator(); + assertEquals(Integer.valueOf(16), iter.next()); + assertEquals(Integer.valueOf(32), iter.next()); + assertEquals(Integer.valueOf(64), iter.next()); + + iter.next(); + fail("Previous iter.next should have failed"); + } + + public void testHasNextIdempotens() throws Exception { + CursorResultSetHandler CursorResultSetHandler = getMockResultSetHandlerForResultList(fakeResultList); + CursorList cursorList = new CursorList(CursorResultSetHandler, getResultSetWrapper(), null, null); + + Iterator iter = cursorList.iterator(); + assertTrue(iter.hasNext()); + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(16), iter.next()); + assertTrue(iter.hasNext()); + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(32), iter.next()); + } + + private CursorResultSetHandler getMockResultSetHandlerForResultList(final int[] fakeResultList) + throws SQLException { + CursorResultSetHandler cursorResultSetHandler = mock(CursorResultSetHandler.class); + doAnswer(new Answer() { + private int currentIndex = 0; + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + ((ResultHandler) args[2]).handleResult(new DefaultResultContext() { + @Override + public Object getResultObject() { + if (currentIndex < fakeResultList.length) { + return Integer.valueOf(fakeResultList[currentIndex++]); + } + return null; + } + }); + return null; + } + }).when(cursorResultSetHandler).handleRowValues(isNotNull(ResultSetWrapper.class), any(ResultMap.class), + any(ResultHandler.class), any(RowBounds.class), any(ResultMapping.class)); + + + + return cursorResultSetHandler; + } + + private ResultSetWrapper getResultSetWrapper() { + ResultSetWrapper resultSetWrapper = mock(ResultSetWrapper.class); + when(resultSetWrapper.getResultSet()).thenReturn(mock(ResultSet.class)); + return resultSetWrapper; + } +} diff --git a/src/test/java/org/apache/ibatis/executor/resultset/LazyListTest.java b/src/test/java/org/apache/ibatis/executor/resultset/LazyListTest.java new file mode 100644 index 00000000000..efc95004cb1 --- /dev/null +++ b/src/test/java/org/apache/ibatis/executor/resultset/LazyListTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2009-2013 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.executor.resultset; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +/** + * @author Guillaume Darmont / guillaume@dropinocean.com + */ +public class LazyListTest { + + private final int[] fakeResultList = new int[] { 16, 32, 64 }; + + @Test + public void checkBaseConstructors() { + CursorList cursorList = mock(CursorList.class); + List list = cursorList; + + LazyList lazyList = new LazyList(cursorList); + assertNotNull(lazyList); + + lazyList = new LazyList(list); + assertNotNull(lazyList); + lazyList = null; + + try { + list = new ArrayList(); + lazyList = new LazyList(list); + fail("Shouldn't be here..."); + } catch (IllegalArgumentException e) { + assertNull(lazyList); + } + } + + @Test(expected = IllegalStateException.class) + public void checkConstructorOnAlreadyUsedCursorList() { + CursorList cursorList = mock(CursorList.class); + when(cursorList.isFetchStarted()).thenReturn(Boolean.TRUE); + + LazyList lazyList = new LazyList(cursorList); + fail("Shouldn't be here..."); + } + + @Test + public void checkCursorListThenLocalStorageAccess() { + CursorList cursorList = mock(CursorList.class); + when(cursorList.fetchNextObjectFromDatabase()).thenReturn(fakeResultList[0]).thenReturn(fakeResultList[1]) + .thenReturn(fakeResultList[2]).thenReturn(null); + + LazyList lazyList = new LazyList(cursorList); + int index = 0; + for (Integer i : lazyList) { + assertEquals(Integer.valueOf(fakeResultList[index++]), i); + } + verify(cursorList, times(4)).fetchNextObjectFromDatabase(); + assertEquals(3, lazyList.size()); + + // Mock cursorList to only return null, so we can check that internal storage is used + reset(cursorList); + when(cursorList.fetchNextObjectFromDatabase()).thenReturn(null); + index = 0; + for (Integer i : lazyList) { + assertEquals(Integer.valueOf(fakeResultList[index++]), i); + } + + verify(cursorList, times(1)).fetchNextObjectFromDatabase(); + assertEquals(3, lazyList.size()); + } + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/CreateDB.sql b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/CreateDB.sql new file mode 100644 index 00000000000..9908d0cab9b --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/CreateDB.sql @@ -0,0 +1,37 @@ + +-- +-- Copyright 2009-2013 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); diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/CursorListNestedTest.java b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/CursorListNestedTest.java new file mode 100644 index 00000000000..fae301c9834 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/CursorListNestedTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2009-2013 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. + */ +package org.apache.ibatis.submitted.cursorlist_nested; + +import java.io.Reader; +import java.sql.Connection; +import java.util.Iterator; +import java.util.List; + +import org.apache.ibatis.executor.resultset.CursorList; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.ScriptRunner; +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; + +public class CursorListNestedTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeClass + public static void setUp() throws Exception { + // create a SqlSessionFactory + Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursorlist_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/cursorlist_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(); + try { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List users = mapper.getAllUsers(); + + Assert.assertTrue(users instanceof CursorList); + + CursorList cursorList = (CursorList) users; + Assert.assertFalse(cursorList.isFetchStarted()); + + // Retrieving iterator, fetching is not started + Iterator iterator = users.iterator(); + Assert.assertFalse(cursorList.isFetchStarted()); + + // Check if hasNext, fetching is started + Assert.assertTrue(iterator.hasNext()); + Assert.assertTrue(cursorList.isFetchStarted()); + Assert.assertFalse(cursorList.isResultSetExhausted()); + + 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()); + + // Check no more elements + Assert.assertTrue(!iterator.hasNext()); + Assert.assertTrue(cursorList.isFetchStarted()); + Assert.assertTrue(cursorList.isResultSetExhausted()); + + } finally { + sqlSession.close(); + } + } + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/Mapper.java b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/Mapper.java new file mode 100644 index 00000000000..e5590d908e9 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/Mapper.java @@ -0,0 +1,24 @@ +/* + * Copyright 2009-2013 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. + */ +package org.apache.ibatis.submitted.cursorlist_nested; + +import java.util.List; + +public interface Mapper { + + List getAllUsers(); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/Mapper.xml b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/Mapper.xml new file mode 100644 index 00000000000..28786a51162 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/Mapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/User.java b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/User.java new file mode 100644 index 00000000000..0c2b29d009a --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/User.java @@ -0,0 +1,58 @@ +/* + * Copyright 2009-2013 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. + */ +package org.apache.ibatis.submitted.cursorlist_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; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/mybatis-config.xml b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/mybatis-config.xml new file mode 100644 index 00000000000..9cdc366fa7c --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_nested/mybatis-config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/CreateDB.sql b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/CreateDB.sql new file mode 100644 index 00000000000..4f2b9f7997d --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/CreateDB.sql @@ -0,0 +1,26 @@ +-- +-- Copyright 2009-2013 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'); diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/CursorListSimpleTest.java b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/CursorListSimpleTest.java new file mode 100644 index 00000000000..a23470bceef --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/CursorListSimpleTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2009-2013 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. + */ +package org.apache.ibatis.submitted.cursorlist_simple; + +import java.io.Reader; +import java.sql.Connection; +import java.util.Iterator; +import java.util.List; + +import org.apache.ibatis.executor.resultset.CursorList; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.jdbc.ScriptRunner; +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; + +public class CursorListSimpleTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeClass + public static void setUp() throws Exception { + // create a SqlSessionFactory + Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursorlist_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/cursorlist_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(); + try { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List users = mapper.getAllUsers(); + + Assert.assertTrue(users instanceof CursorList); + + CursorList cursorList = (CursorList) users; + Assert.assertFalse(cursorList.isFetchStarted()); + + // Retrieving iterator, fetching is not started + Iterator iterator = users.iterator(); + Assert.assertFalse(cursorList.isFetchStarted()); + + // Check if hasNext, fetching is started + Assert.assertTrue(iterator.hasNext()); + Assert.assertTrue(cursorList.isFetchStarted()); + Assert.assertFalse(cursorList.isResultSetExhausted()); + + User user = iterator.next(); + Assert.assertEquals("User1", user.getName()); + user = iterator.next(); + Assert.assertEquals("User2", user.getName()); + user = iterator.next(); + Assert.assertEquals("User3", user.getName()); + + // Check no more elements + Assert.assertTrue(!iterator.hasNext()); + Assert.assertTrue(cursorList.isFetchStarted()); + Assert.assertTrue(cursorList.isResultSetExhausted()); + + } finally { + sqlSession.close(); + } + } + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/Mapper.java b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/Mapper.java new file mode 100644 index 00000000000..c9bdec2fdf5 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/Mapper.java @@ -0,0 +1,24 @@ +/* + * Copyright 2009-2013 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. + */ +package org.apache.ibatis.submitted.cursorlist_simple; + +import java.util.List; + +public interface Mapper { + + List getAllUsers(); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/Mapper.xml b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/Mapper.xml new file mode 100644 index 00000000000..0926fb27093 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/Mapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/User.java b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/User.java new file mode 100644 index 00000000000..5156da12a61 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/User.java @@ -0,0 +1,38 @@ +/* + * Copyright 2009-2013 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. + */ +package org.apache.ibatis.submitted.cursorlist_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; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/mybatis-config.xml b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/mybatis-config.xml new file mode 100644 index 00000000000..c0199328ed8 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/cursorlist_simple/mybatis-config.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + +