View Javadoc
1   /*
2    *    Copyright 2009-2024 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    *       https://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.submitted.cursor_simple;
17  
18  import java.io.IOException;
19  import java.io.Reader;
20  import java.util.ArrayList;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.NoSuchElementException;
24  
25  import org.apache.ibatis.BaseDataTest;
26  import org.apache.ibatis.cursor.Cursor;
27  import org.apache.ibatis.io.Resources;
28  import org.apache.ibatis.session.RowBounds;
29  import org.apache.ibatis.session.SqlSession;
30  import org.apache.ibatis.session.SqlSessionFactory;
31  import org.apache.ibatis.session.SqlSessionFactoryBuilder;
32  import org.junit.jupiter.api.Assertions;
33  import org.junit.jupiter.api.BeforeAll;
34  import org.junit.jupiter.api.MethodOrderer;
35  import org.junit.jupiter.api.Test;
36  import org.junit.jupiter.api.TestMethodOrder;
37  
38  @TestMethodOrder(MethodOrderer.MethodName.class)
39  class CursorSimpleTest {
40  
41    private static SqlSessionFactory sqlSessionFactory;
42  
43    @BeforeAll
44    static void setUp() throws Exception {
45      // create a SqlSessionFactory
46      try (
47          Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/cursor_simple/mybatis-config.xml")) {
48        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
49      }
50  
51      // populate in-memory database
52      BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
53          "org/apache/ibatis/submitted/cursor_simple/CreateDB.sql");
54    }
55  
56    @Test
57    void shouldGetAllUser() {
58      try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
59        Mapper mapper = sqlSession.getMapper(Mapper.class);
60        Cursor<User> usersCursor = mapper.getAllUsers();
61  
62        Assertions.assertFalse(usersCursor.isOpen());
63  
64        // Cursor is just created, current index is -1
65        Assertions.assertEquals(-1, usersCursor.getCurrentIndex());
66  
67        Iterator<User> iterator = usersCursor.iterator();
68  
69        // Check if hasNext, fetching is started
70        Assertions.assertTrue(iterator.hasNext());
71        Assertions.assertTrue(usersCursor.isOpen());
72        Assertions.assertFalse(usersCursor.isConsumed());
73  
74        // next() has not been called, index is still -1
75        Assertions.assertEquals(-1, usersCursor.getCurrentIndex());
76  
77        User user = iterator.next();
78        Assertions.assertEquals("User1", user.getName());
79        Assertions.assertEquals(0, usersCursor.getCurrentIndex());
80  
81        user = iterator.next();
82        Assertions.assertEquals("User2", user.getName());
83        Assertions.assertEquals(1, usersCursor.getCurrentIndex());
84  
85        user = iterator.next();
86        Assertions.assertEquals("User3", user.getName());
87        Assertions.assertEquals(2, usersCursor.getCurrentIndex());
88  
89        user = iterator.next();
90        Assertions.assertEquals("User4", user.getName());
91        Assertions.assertEquals(3, usersCursor.getCurrentIndex());
92  
93        user = iterator.next();
94        Assertions.assertEquals("User5", user.getName());
95        Assertions.assertEquals(4, usersCursor.getCurrentIndex());
96  
97        // Check no more elements
98        Assertions.assertFalse(iterator.hasNext());
99        Assertions.assertFalse(usersCursor.isOpen());
100       Assertions.assertTrue(usersCursor.isConsumed());
101     }
102   }
103 
104   @Test
105   void cursorClosedOnSessionClose() {
106     Cursor<User> usersCursor;
107     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
108       Mapper mapper = sqlSession.getMapper(Mapper.class);
109       usersCursor = mapper.getAllUsers();
110 
111       Assertions.assertFalse(usersCursor.isOpen());
112 
113       Iterator<User> iterator = usersCursor.iterator();
114 
115       // Check if hasNext, fetching is started
116       Assertions.assertTrue(iterator.hasNext());
117       Assertions.assertTrue(usersCursor.isOpen());
118       Assertions.assertFalse(usersCursor.isConsumed());
119 
120       // Consume only the first result
121       User user = iterator.next();
122       Assertions.assertEquals("User1", user.getName());
123 
124       // Check there is still remaining elements
125       Assertions.assertTrue(iterator.hasNext());
126       Assertions.assertTrue(usersCursor.isOpen());
127       Assertions.assertFalse(usersCursor.isConsumed());
128     }
129 
130     // The cursor was not fully consumed, but it should be close since we closed the session
131     Assertions.assertFalse(usersCursor.isOpen());
132     Assertions.assertFalse(usersCursor.isConsumed());
133   }
134 
135   @Test
136   void cursorWithRowBound() {
137     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
138       // RowBound starting at offset 1 and limiting to 2 items
139       Cursor<User> usersCursor = sqlSession.selectCursor("getAllUsers", null, new RowBounds(1, 3));
140 
141       Iterator<User> iterator = usersCursor.iterator();
142 
143       User user = iterator.next();
144       Assertions.assertEquals("User2", user.getName());
145       Assertions.assertEquals(1, usersCursor.getCurrentIndex());
146 
147       // Calling hasNext() before next()
148       Assertions.assertTrue(iterator.hasNext());
149       user = iterator.next();
150       Assertions.assertEquals("User3", user.getName());
151       Assertions.assertEquals(2, usersCursor.getCurrentIndex());
152 
153       // Calling next() without a previous hasNext() call
154       user = iterator.next();
155       Assertions.assertEquals("User4", user.getName());
156       Assertions.assertEquals(3, usersCursor.getCurrentIndex());
157 
158       Assertions.assertFalse(iterator.hasNext());
159       Assertions.assertFalse(usersCursor.isOpen());
160       Assertions.assertTrue(usersCursor.isConsumed());
161     }
162   }
163 
164   @Test
165   void cursorIteratorNoSuchElementExceptionWithHasNext() throws IOException {
166     try (SqlSession sqlSession = sqlSessionFactory.openSession();
167         Cursor<User> usersCursor = sqlSession.selectCursor("getAllUsers", null, new RowBounds(1, 1))) {
168       try {
169         Iterator<User> iterator = usersCursor.iterator();
170 
171         User user = iterator.next();
172         Assertions.assertEquals("User2", user.getName());
173         Assertions.assertEquals(1, usersCursor.getCurrentIndex());
174 
175         Assertions.assertFalse(iterator.hasNext());
176         iterator.next();
177         Assertions.fail("We should have failed since we call next() when hasNext() returned false");
178       } catch (NoSuchElementException e) {
179         Assertions.assertFalse(usersCursor.isOpen());
180         Assertions.assertTrue(usersCursor.isConsumed());
181       }
182     }
183   }
184 
185   @Test
186   void cursorIteratorNoSuchElementExceptionNoHasNext() throws IOException {
187     try (SqlSession sqlSession = sqlSessionFactory.openSession();
188         Cursor<User> usersCursor = sqlSession.selectCursor("getAllUsers", null, new RowBounds(1, 1))) {
189       try {
190         Iterator<User> iterator = usersCursor.iterator();
191         User user = iterator.next();
192         Assertions.assertEquals("User2", user.getName());
193         Assertions.assertEquals(1, usersCursor.getCurrentIndex());
194 
195         // Trying next() without hasNext()
196         iterator.next();
197         Assertions.fail("We should have failed since we call next() when is no more items");
198       } catch (NoSuchElementException e) {
199         Assertions.assertFalse(usersCursor.isOpen());
200         Assertions.assertTrue(usersCursor.isConsumed());
201       }
202     }
203   }
204 
205   @Test
206   void cursorWithBadRowBound() {
207     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
208       // Trying to start at offset 10 (which does not exist, since there is only 4 items)
209       Cursor<User> usersCursor = sqlSession.selectCursor("getAllUsers", null, new RowBounds(10, 2));
210       Iterator<User> iterator = usersCursor.iterator();
211 
212       Assertions.assertFalse(iterator.hasNext());
213       Assertions.assertFalse(usersCursor.isOpen());
214       Assertions.assertTrue(usersCursor.isConsumed());
215     }
216   }
217 
218   @Test
219   void cursorMultipleHasNextCall() {
220     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
221       Mapper mapper = sqlSession.getMapper(Mapper.class);
222       Cursor<User> usersCursor = mapper.getAllUsers();
223 
224       Iterator<User> iterator = usersCursor.iterator();
225 
226       Assertions.assertEquals(-1, usersCursor.getCurrentIndex());
227 
228       User user = iterator.next();
229       Assertions.assertEquals("User1", user.getName());
230       Assertions.assertEquals(0, usersCursor.getCurrentIndex());
231 
232       Assertions.assertTrue(iterator.hasNext());
233       Assertions.assertTrue(iterator.hasNext());
234       Assertions.assertTrue(iterator.hasNext());
235       // assert that index has not changed after hasNext() call
236       Assertions.assertEquals(0, usersCursor.getCurrentIndex());
237     }
238   }
239 
240   @Test
241   void cursorMultipleIteratorCall() {
242     Iterator<User> iterator2 = null;
243     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
244       Mapper mapper = sqlSession.getMapper(Mapper.class);
245       Cursor<User> usersCursor = mapper.getAllUsers();
246 
247       Iterator<User> iterator = usersCursor.iterator();
248       User user = iterator.next();
249       Assertions.assertEquals("User1", user.getName());
250       Assertions.assertEquals(0, usersCursor.getCurrentIndex());
251 
252       iterator2 = usersCursor.iterator();
253       iterator2.hasNext();
254       Assertions.fail("We should have failed since calling iterator several times is not allowed");
255     } catch (IllegalStateException e) {
256       Assertions.assertNull(iterator2, "iterator2 should be null");
257       return;
258     }
259     Assertions.fail("Should have returned earlier");
260   }
261 
262   @Test
263   void cursorMultipleCloseCall() throws IOException {
264     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
265       Mapper mapper = sqlSession.getMapper(Mapper.class);
266       Cursor<User> usersCursor = mapper.getAllUsers();
267 
268       Assertions.assertFalse(usersCursor.isOpen());
269 
270       Iterator<User> iterator = usersCursor.iterator();
271 
272       // Check if hasNext, fetching is started
273       Assertions.assertTrue(iterator.hasNext());
274       Assertions.assertTrue(usersCursor.isOpen());
275       Assertions.assertFalse(usersCursor.isConsumed());
276 
277       // Consume only the first result
278       User user = iterator.next();
279       Assertions.assertEquals("User1", user.getName());
280 
281       usersCursor.close();
282       // Check multiple close are no-op
283       usersCursor.close();
284 
285       // hasNext now return false, since the cursor is closed
286       Assertions.assertFalse(iterator.hasNext());
287       Assertions.assertFalse(usersCursor.isOpen());
288       Assertions.assertFalse(usersCursor.isConsumed());
289     }
290   }
291 
292   @Test
293   void cursorUsageAfterClose() throws IOException {
294 
295     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
296       Mapper mapper = sqlSession.getMapper(Mapper.class);
297 
298       Cursor<User> usersCursor = mapper.getAllUsers();
299       try {
300         Iterator<User> iterator = usersCursor.iterator();
301         User user = iterator.next();
302         Assertions.assertEquals("User1", user.getName());
303         Assertions.assertEquals(0, usersCursor.getCurrentIndex());
304 
305         user = iterator.next();
306         Assertions.assertEquals("User2", user.getName());
307         Assertions.assertEquals(1, usersCursor.getCurrentIndex());
308 
309         usersCursor.close();
310 
311         // hasNext now return false, since the cursor is closed
312         Assertions.assertFalse(iterator.hasNext());
313         Assertions.assertFalse(usersCursor.isOpen());
314         Assertions.assertFalse(usersCursor.isConsumed());
315 
316         // trying next() will fail
317         iterator.next();
318 
319         Assertions.fail("We should have failed with NoSuchElementException since Cursor is closed");
320       } catch (NoSuchElementException e) {
321         // We had an exception and current index has not changed
322         Assertions.assertEquals(1, usersCursor.getCurrentIndex());
323         usersCursor.close();
324         return;
325       }
326     }
327 
328     Assertions.fail("Should have returned earlier");
329   }
330 
331   @Test
332   void shouldGetAllUserUsingAnnotationBasedMapper() {
333     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
334       sqlSession.getConfiguration().getMapperRegistry().addMapper(AnnotationMapper.class);
335       AnnotationMapper mapper = sqlSession.getMapper(AnnotationMapper.class);
336       Cursor<User> usersCursor = mapper.getAllUsers();
337 
338       Assertions.assertFalse(usersCursor.isOpen());
339       Assertions.assertFalse(usersCursor.isConsumed());
340       Assertions.assertEquals(-1, usersCursor.getCurrentIndex());
341 
342       List<User> userList = new ArrayList<>();
343       for (User user : usersCursor) {
344         userList.add(user);
345         Assertions.assertEquals(userList.size() - 1, usersCursor.getCurrentIndex());
346       }
347 
348       Assertions.assertFalse(usersCursor.isOpen());
349       Assertions.assertTrue(usersCursor.isConsumed());
350       Assertions.assertEquals(4, usersCursor.getCurrentIndex());
351 
352       Assertions.assertEquals(5, userList.size());
353       User user = userList.get(0);
354       Assertions.assertEquals("User1", user.getName());
355       user = userList.get(1);
356       Assertions.assertEquals("User2", user.getName());
357       user = userList.get(2);
358       Assertions.assertEquals("User3", user.getName());
359       user = userList.get(3);
360       Assertions.assertEquals("User4", user.getName());
361       user = userList.get(4);
362       Assertions.assertEquals("User5", user.getName());
363     }
364   }
365 
366   @Test
367   void shouldThrowIllegalStateExceptionUsingIteratorOnSessionClosed() {
368     Cursor<User> usersCursor;
369     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
370       usersCursor = sqlSession.getMapper(Mapper.class).getAllUsers();
371     }
372     try {
373       usersCursor.iterator();
374       Assertions.fail("Should throws the IllegalStateException when call the iterator method after session is closed.");
375     } catch (IllegalStateException e) {
376       Assertions.assertEquals("A Cursor is already closed.", e.getMessage());
377     }
378 
379     // verify for checking order
380     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
381       usersCursor = sqlSession.getMapper(Mapper.class).getAllUsers();
382       usersCursor.iterator();
383     }
384     try {
385       usersCursor.iterator();
386       Assertions.fail("Should throws the IllegalStateException when call the iterator already.");
387     } catch (IllegalStateException e) {
388       Assertions.assertEquals("Cannot open more than one iterator on a Cursor", e.getMessage());
389     }
390 
391   }
392 
393   @Test
394   void shouldNullItemNotStopIteration() {
395     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
396       Mapper mapper = sqlSession.getMapper(Mapper.class);
397       Cursor<User> cursor = mapper.getNullUsers(new RowBounds());
398       Iterator<User> iterator = cursor.iterator();
399 
400       Assertions.assertFalse(cursor.isOpen());
401 
402       // Cursor is just created, current index is -1
403       Assertions.assertEquals(-1, cursor.getCurrentIndex());
404 
405       // Check if hasNext, fetching is started
406       Assertions.assertTrue(iterator.hasNext());
407       // Re-invoking hasNext() should not fetch the next row
408       Assertions.assertTrue(iterator.hasNext());
409       Assertions.assertTrue(cursor.isOpen());
410       Assertions.assertFalse(cursor.isConsumed());
411 
412       // next() has not been called, index is still -1
413       Assertions.assertEquals(-1, cursor.getCurrentIndex());
414 
415       User user = iterator.next();
416       Assertions.assertNull(user);
417       Assertions.assertEquals(0, cursor.getCurrentIndex());
418 
419       Assertions.assertTrue(iterator.hasNext());
420       user = iterator.next();
421       Assertions.assertEquals("Kate", user.getName());
422       Assertions.assertEquals(1, cursor.getCurrentIndex());
423 
424       Assertions.assertTrue(iterator.hasNext());
425       user = iterator.next();
426       Assertions.assertNull(user);
427       Assertions.assertEquals(2, cursor.getCurrentIndex());
428 
429       Assertions.assertTrue(iterator.hasNext());
430       user = iterator.next();
431       Assertions.assertNull(user);
432       Assertions.assertEquals(3, cursor.getCurrentIndex());
433 
434       // Check no more elements
435       Assertions.assertFalse(iterator.hasNext());
436       Assertions.assertFalse(cursor.isOpen());
437       Assertions.assertTrue(cursor.isConsumed());
438     }
439   }
440 
441   @Test
442   void shouldRowBoundsCountNullItem() {
443     try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
444       Mapper mapper = sqlSession.getMapper(Mapper.class);
445       Cursor<User> cursor = mapper.getNullUsers(new RowBounds(1, 2));
446       Iterator<User> iterator = cursor.iterator();
447 
448       Assertions.assertFalse(cursor.isOpen());
449 
450       // Check if hasNext, fetching is started
451       Assertions.assertTrue(iterator.hasNext());
452       // Re-invoking hasNext() should not fetch the next row
453       Assertions.assertTrue(iterator.hasNext());
454       Assertions.assertTrue(cursor.isOpen());
455       Assertions.assertFalse(cursor.isConsumed());
456 
457       User user = iterator.next();
458       Assertions.assertEquals("Kate", user.getName());
459       Assertions.assertEquals(1, cursor.getCurrentIndex());
460 
461       Assertions.assertTrue(iterator.hasNext());
462       user = iterator.next();
463       Assertions.assertNull(user);
464       Assertions.assertEquals(2, cursor.getCurrentIndex());
465 
466       // Check no more elements
467       Assertions.assertFalse(iterator.hasNext());
468       Assertions.assertFalse(cursor.isOpen());
469       Assertions.assertTrue(cursor.isConsumed());
470     }
471   }
472 }