mybatis/mybatis-3 · error · ExecutorException

Mapped Statements with nested result mappings cannot be safe

Error message

Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. Use safeResultHandlerEnabled=false setting to bypass this check or ensure your statement returns ordered data and set resultOrdered=true on it.

What it means

Nested resultMap mapping groups rows in memory, so a custom ResultHandler receiving partial groups would see incomplete parent objects. When safeResultHandlerEnabled=true (default true) and a custom resultHandler is supplied for a nested-mapping statement that is not marked resultOrdered=true, checkResultHandler throws.

Source

Thrown at src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:389

      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

  private void ensureNoRowBounds() {
    if (configuration.isSafeRowBoundsEnabled() && rowBounds != null
        && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) {
      throw new ExecutorException(
          "Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. "
              + "Use safeRowBoundsEnabled=false setting to bypass this check.");
    }
  }

  protected void checkResultHandler() {
    if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
      throw new ExecutorException(
          "Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
              + "Use safeResultHandlerEnabled=false setting to bypass this check "
              + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
    }
  }

  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
      ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    final boolean useCollectionConstructorInjection = resultMap.hasResultMapsUsingConstructorCollection();

    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw, resultMap, null);
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null, null);
      if (!useCollectionConstructorInjection) {
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);

View on GitHub (pinned to 008069adb1)

Solutions

  1. If the SQL returns all rows of a parent contiguously (ordered by parent key), set resultOrdered="true" on the <select> to declare safety
  2. Otherwise use the default handler (selectList) for nested-mapped statements and post-process the returned graph
  3. As last resort set <setting name="safeResultHandlerEnabled" value="false"/> and accept partial-group risk

Example fix

<!-- before -->
<select id="selectOrdersWithItems" resultMap="orderWithItemsMap">
  SELECT * FROM orders o JOIN items i ON i.order_id = o.id
</select>
<!-- after: ORDER BY guarantees parents arrive contiguously -->
<select id="selectOrdersWithItems" resultMap="orderWithItemsMap" resultOrdered="true">
  SELECT * FROM orders o JOIN items i ON i.order_id = o.id ORDER BY o.id
</select>
Defensive patterns

Strategy: validation

Validate before calling

MappedStatement ms = session.getConfiguration().getMappedStatement("sel.ordersWithItems");
boolean nested = ms.getResultMaps().stream().anyMatch(ResultMap::hasNestedResultMaps);
if (nested && customHandler != null && !ms.isResultOrdered()) {
  throw new IllegalArgumentException("Set resultOrdered=true or use selectList for " + ms.getId());
}

Try / catch

try { session.select(stmt, param, handler); } catch (ExecutorException e) { if (e.getMessage().contains("custom ResultHandler")) { session.selectList(stmt, param); /* then post-process */ } else throw e; }

Prevention

When it happens

Trigger: handleRowValues with a resultMap having nested resultMaps, resultHandler != null, configuration.isSafeResultHandlerEnabled(), and mappedStatement.isResultOrdered()==false — e.g. session.select(statement, param, myResultHandler) on a JOIN-mapped query.

Common situations: Streaming results with a custom ResultHandler onto statements that use <collection>/<association> resultMap nesting; forgetting resultOrdered='true' on ordered-join statements that the developer knows are safe.

Related errors


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