mybatis/mybatis-3 · error · IllegalStateException

Cannot open more than one iterator on a Cursor

Error message

Cannot open more than one iterator on a Cursor

What it means

A MyBatis Cursor (streaming result iterator returned by SqlSession.selectCursor) allows only ONE Iterator to be opened during its lifetime. The Cursor class implements Iterable by returning its internal single iterator, and sets iteratorRetrieved=true on the first call. Calling iterator() again (directly, via for-each, or via forEach) throws IllegalStateException because two concurrent iterators would share and corrupt the same underlying ResultSet.

Source

Thrown at src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java:97

  @Override
  public boolean isOpen() {
    return status == CursorStatus.OPEN;
  }

  @Override
  public boolean isConsumed() {
    return status == CursorStatus.CONSUMED;
  }

  @Override
  public int getCurrentIndex() {
    return rowBounds.getOffset() + cursorIterator.iteratorIndex;
  }

  @Override
  public Iterator<T> iterator() {
    if (iteratorRetrieved) {
      throw new IllegalStateException("Cannot open more than one iterator on a Cursor");
    }
    if (isClosed()) {
      throw new IllegalStateException("A Cursor is already closed.");
    }
    iteratorRetrieved = true;
    return cursorIterator;
  }

  @Override
  public void close() {
    if (isClosed()) {
      return;
    }

    ResultSet rs = rsw.getResultSet();
    try {
      if (rs != null) {
        rs.close();

View on GitHub (pinned to 008069adb1)

Solutions

  1. Fetch a new Cursor via a fresh sqlSession.selectCursor(...) call for each iteration pass
  2. Materialize the results into a List once (e.g., new ArrayList<>(cursor) or cursor.stream().collect(toList())) if you need multiple passes
  3. Track iteration yourself: only call iterator() once per Cursor instance and consume it fully (or close it) before discarding
  4. If you need resumable streaming, use getCurrentIndex() to record position and re-query with a RowBounds offset from a new Cursor

Example fix

// before
try (Cursor<User> c = sqlSession.selectCursor("findUsers")) {
  c.forEach(u -> log.debug(u));           // iterator #1
  for (User u : c) process(u);            // iterator #2 -> IllegalStateException
}

// after
try (Cursor<User> c = sqlSession.selectCursor("findUsers")) {
  List<User> users = new ArrayList<>();
  c.forEach(users::add);                  // single iteration
  users.forEach(u -> log.debug(u));
  users.forEach(this::process);
}
Defensive patterns

Strategy: validation

Validate before calling

// DefaultCursor tracks retrieval internally but exposes no getter;
// wrap it so iteration happens exactly once per Cursor.
boolean iterated = false;
try (Cursor<User> c = sqlSession.selectCursor("findUsers")) {
  if (iterated) throw new IllegalStateException("cursor already iterated");
  iterated = true;
  c.forEach(this::process);
}

Try / catch

// Only if a stray second iteration is possible:
try {
  for (User u : cursor) { ... }
} catch (IllegalStateException e) {
  if (e.getMessage().contains("more than one iterator")) {
    // re-open: fetch a fresh Cursor from a new selectCursor call
  } else throw e;
}

Prevention

When it happens

Trigger: Calling cursor.iterator() a second time; using the same Cursor object in two enhanced for-loops; calling cursor.forEach(x -> ...) after already iterating it with for-each; passing the Cursor to a method that iterates it after your code already did; re-streaming a partially consumed Cursor in a retry loop.

Common situations: Streaming large result sets with selectCursor and accidentally iterating twice (e.g., once for logging/counting, once for processing); helper utilities that accept Iterable and iterate internally; retry logic that re-consumes the same Cursor; Spring code that wraps the Cursor in a Stream and also loops over it.

Related errors


AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14). Data as JSON: /api/errors/1c64f051b354e2e4. Report an issue: GitHub.