Skip to content

Commit 1e6515d

Browse files
committed
Merge pull request mybatis#437 from gdarmont/pr-cursor
Cursor list implementation
2 parents 239c182 + 58f92e5 commit 1e6515d

34 files changed

+1144
-24
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
</parent>
2525

2626
<artifactId>mybatis</artifactId>
27-
<version>3.3.1-SNAPSHOT</version>
27+
<version>3.4.0-SNAPSHOT</version>
2828
<packaging>jar</packaging>
2929

3030
<name>mybatis</name>

src/main/java/org/apache/ibatis/binding/MapperMethod.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.apache.ibatis.annotations.Flush;
1919
import org.apache.ibatis.annotations.MapKey;
2020
import org.apache.ibatis.annotations.Param;
21+
import org.apache.ibatis.cursor.Cursor;
2122
import org.apache.ibatis.mapping.MappedStatement;
2223
import org.apache.ibatis.mapping.SqlCommandType;
2324
import org.apache.ibatis.reflection.MetaObject;
@@ -64,6 +65,8 @@ public Object execute(SqlSession sqlSession, Object[] args) {
6465
result = executeForMany(sqlSession, args);
6566
} else if (method.returnsMap()) {
6667
result = executeForMap(sqlSession, args);
68+
} else if (method.returnsCursor()) {
69+
result = executeForCursor(sqlSession, args);
6770
} else {
6871
Object param = method.convertArgsToSqlCommandParam(args);
6972
result = sqlSession.selectOne(command.getName(), param);
@@ -132,6 +135,18 @@ private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
132135
return result;
133136
}
134137

138+
private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
139+
Cursor<T> result;
140+
Object param = method.convertArgsToSqlCommandParam(args);
141+
if (method.hasRowBounds()) {
142+
RowBounds rowBounds = method.extractRowBounds(args);
143+
result = sqlSession.<T>selectCursor(command.getName(), param, rowBounds);
144+
} else {
145+
result = sqlSession.<T>selectCursor(command.getName(), param);
146+
}
147+
return result;
148+
}
149+
135150
private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
136151
Object collection = config.getObjectFactory().create(method.getReturnType());
137152
MetaObject metaObject = config.newMetaObject(collection);
@@ -218,6 +233,7 @@ public static class MethodSignature {
218233
private final boolean returnsMany;
219234
private final boolean returnsMap;
220235
private final boolean returnsVoid;
236+
private final boolean returnsCursor;
221237
private final Class<?> returnType;
222238
private final String mapKey;
223239
private final Integer resultHandlerIndex;
@@ -229,6 +245,7 @@ public MethodSignature(Configuration configuration, Method method) {
229245
this.returnType = method.getReturnType();
230246
this.returnsVoid = void.class.equals(this.returnType);
231247
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
248+
this.returnsCursor = Cursor.class.equals(this.returnType);
232249
this.mapKey = getMapKey(method);
233250
this.returnsMap = (this.mapKey != null);
234251
this.hasNamedParameters = hasNamedParams(method);
@@ -295,6 +312,10 @@ public boolean returnsVoid() {
295312
return returnsVoid;
296313
}
297314

315+
public boolean returnsCursor() {
316+
return returnsCursor;
317+
}
318+
298319
private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
299320
Integer index = null;
300321
final Class<?>[] argTypes = method.getParameterTypes();
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright 2009-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.ibatis.cursor;
17+
18+
import java.io.Closeable;
19+
20+
/**
21+
* Cursor contract to handle fetching items lazily using an Iterator.
22+
* Cursors are a perfect fit to handle millions of items queries that would not normally fits in memory.
23+
* Cursor SQL queries must be ordered (resultOrdered="true") using the id columns of the resultMap.
24+
*
25+
* @author Guillaume Darmont / guillaume@dropinocean.com
26+
*/
27+
public interface Cursor<T> extends Closeable, Iterable<T> {
28+
29+
/**
30+
* @return true if the cursor has started to fetch items from database.
31+
*/
32+
boolean isOpen();
33+
34+
/**
35+
*
36+
* @return true if the cursor is fully consumed and has returned all elements matching the query.
37+
*/
38+
boolean isConsumed();
39+
40+
/**
41+
* Get the current item index. The first item has the index 0.
42+
* @return -1 if the cursor is not open and has not been consumed. The index of the current item retrieved.
43+
*/
44+
int getCurrentIndex();
45+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Copyright 2009-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.ibatis.cursor.defaults;
17+
18+
import org.apache.ibatis.cursor.Cursor;
19+
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
20+
import org.apache.ibatis.executor.resultset.ResultSetWrapper;
21+
import org.apache.ibatis.mapping.ResultMap;
22+
import org.apache.ibatis.session.ResultContext;
23+
import org.apache.ibatis.session.ResultHandler;
24+
import org.apache.ibatis.session.RowBounds;
25+
26+
import java.sql.ResultSet;
27+
import java.sql.SQLException;
28+
import java.sql.Statement;
29+
import java.util.Iterator;
30+
import java.util.NoSuchElementException;
31+
32+
/**
33+
* @author Guillaume Darmont / guillaume@dropinocean.com
34+
*/
35+
public class DefaultCursor<T> implements Cursor<T> {
36+
37+
// ResultSetHandler stuff
38+
private final DefaultResultSetHandler resultSetHandler;
39+
private final ResultMap resultMap;
40+
private final ResultSetWrapper rsw;
41+
private final RowBounds rowBounds;
42+
private final ObjectWrapperResultHandler<T> objectWrapperResultHandler = new ObjectWrapperResultHandler<T>();
43+
44+
private int currentIndex = -1;
45+
46+
private boolean iteratorAlreadyOpened = false;
47+
48+
private boolean opened = false;
49+
50+
private boolean resultSetConsumed = false;
51+
52+
public DefaultCursor(DefaultResultSetHandler resultSetHandler, ResultMap resultMap, ResultSetWrapper rsw, RowBounds rowBounds) {
53+
this.resultSetHandler = resultSetHandler;
54+
this.resultMap = resultMap;
55+
this.rsw = rsw;
56+
this.rowBounds = rowBounds;
57+
}
58+
59+
@Override
60+
public boolean isOpen() {
61+
return opened;
62+
}
63+
64+
@Override
65+
public boolean isConsumed() {
66+
return resultSetConsumed;
67+
}
68+
69+
@Override
70+
public int getCurrentIndex() {
71+
return currentIndex;
72+
}
73+
74+
@Override
75+
public Iterator<T> iterator() {
76+
return new CursorIterator();
77+
}
78+
79+
@Override
80+
public void close() {
81+
ResultSet rs = rsw.getResultSet();
82+
try {
83+
if (rs != null) {
84+
Statement statement = rs.getStatement();
85+
86+
rs.close();
87+
if (statement != null) {
88+
statement.close();
89+
}
90+
}
91+
opened = false;
92+
} catch (SQLException e) {
93+
// ignore
94+
}
95+
}
96+
97+
protected T fetchNextUsingRowBound() {
98+
T result = fetchNextObjectFromDatabase();
99+
while (currentIndex < rowBounds.getOffset()) {
100+
result = fetchNextObjectFromDatabase();
101+
}
102+
return result;
103+
}
104+
105+
protected T fetchNextObjectFromDatabase() {
106+
if (resultSetConsumed) {
107+
return null;
108+
}
109+
110+
try {
111+
opened = true;
112+
resultSetHandler.handleRowValues(rsw, resultMap, objectWrapperResultHandler, RowBounds.DEFAULT, null);
113+
} catch (SQLException e) {
114+
throw new RuntimeException(e);
115+
}
116+
117+
T next = objectWrapperResultHandler.result;
118+
if (next != null) {
119+
currentIndex++;
120+
}
121+
// No more object or limit reached
122+
if (next == null || (getReadItemsCount() == rowBounds.getOffset() + rowBounds.getLimit())) {
123+
close();
124+
resultSetConsumed = true;
125+
}
126+
objectWrapperResultHandler.result = null;
127+
128+
return next;
129+
}
130+
131+
private int getReadItemsCount() {
132+
return currentIndex + 1;
133+
}
134+
135+
private static class ObjectWrapperResultHandler<E> implements ResultHandler {
136+
137+
private E result;
138+
139+
@Override
140+
public void handleResult(ResultContext context) {
141+
this.result = (E) context.getResultObject();
142+
context.stop();
143+
}
144+
}
145+
146+
private class CursorIterator implements Iterator<T> {
147+
148+
/**
149+
* Holder for the next objet to be returned
150+
*/
151+
T object;
152+
153+
public CursorIterator() {
154+
if (iteratorAlreadyOpened) {
155+
throw new IllegalStateException("Cannot open more than one iterator on a Cursor");
156+
}
157+
iteratorAlreadyOpened = true;
158+
}
159+
160+
@Override
161+
public boolean hasNext() {
162+
if (object == null) {
163+
object = fetchNextUsingRowBound();
164+
}
165+
return object != null;
166+
}
167+
168+
@Override
169+
public T next() {
170+
// Fill next with object fetched from hasNext()
171+
T next = object;
172+
173+
if (next == null) {
174+
next = fetchNextUsingRowBound();
175+
}
176+
177+
if (next != null) {
178+
object = null;
179+
return next;
180+
}
181+
throw new NoSuchElementException();
182+
}
183+
184+
@Override
185+
public void remove() {
186+
throw new UnsupportedOperationException("Cannot currently remove element from Cursor");
187+
}
188+
}
189+
}

src/main/java/org/apache/ibatis/executor/BaseExecutor.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
import org.apache.ibatis.cache.CacheKey;
2727
import org.apache.ibatis.cache.impl.PerpetualCache;
28+
import org.apache.ibatis.cursor.Cursor;
29+
import org.apache.ibatis.jdbc.SQL;
2830
import org.apache.ibatis.logging.Log;
2931
import org.apache.ibatis.logging.LogFactory;
3032
import org.apache.ibatis.logging.jdbc.ConnectionLogger;
@@ -170,6 +172,12 @@ public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBoun
170172
return list;
171173
}
172174

175+
@Override
176+
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
177+
BoundSql boundSql = ms.getBoundSql(parameter);
178+
return doQueryCursor(ms, parameter, rowBounds, boundSql);
179+
}
180+
173181
@Override
174182
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
175183
if (closed) {
@@ -219,7 +227,7 @@ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBo
219227
cacheKey.update(configuration.getEnvironment().getId());
220228
}
221229
return cacheKey;
222-
}
230+
}
223231

224232
@Override
225233
public boolean isCached(MappedStatement ms, CacheKey key) {
@@ -269,6 +277,9 @@ protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
269277
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
270278
throws SQLException;
271279

280+
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
281+
throws SQLException;
282+
272283
protected void closeStatement(Statement statement) {
273284
if (statement != null) {
274285
try {

src/main/java/org/apache/ibatis/executor/BatchExecutor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Collections;
2424
import java.util.List;
2525

26+
import org.apache.ibatis.cursor.Cursor;
2627
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
2728
import org.apache.ibatis.executor.keygen.KeyGenerator;
2829
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
@@ -94,6 +95,17 @@ public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds
9495
}
9596
}
9697

98+
@Override
99+
protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException {
100+
flushStatements();
101+
Configuration configuration = ms.getConfiguration();
102+
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
103+
Connection connection = getConnection(ms.getStatementLog());
104+
Statement stmt = handler.prepare(connection);
105+
handler.parameterize(stmt);
106+
return handler.<E>queryCursor(stmt);
107+
}
108+
97109
@Override
98110
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
99111
try {

src/main/java/org/apache/ibatis/executor/CachingExecutor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.ibatis.cache.Cache;
2222
import org.apache.ibatis.cache.CacheKey;
2323
import org.apache.ibatis.cache.TransactionalCacheManager;
24+
import org.apache.ibatis.cursor.Cursor;
2425
import org.apache.ibatis.mapping.BoundSql;
2526
import org.apache.ibatis.mapping.MappedStatement;
2627
import org.apache.ibatis.mapping.ParameterMapping;
@@ -82,6 +83,12 @@ public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds r
8283
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
8384
}
8485

86+
@Override
87+
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
88+
flushCacheIfRequired(ms);
89+
return delegate.queryCursor(ms, parameter, rowBounds);
90+
}
91+
8592
@Override
8693
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
8794
throws SQLException {

0 commit comments

Comments
 (0)