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 constrained by RowBounds. Use safeRowBoundsEnabled=false setting to bypass this check.

What it means

With nested resultMaps (JOIN-based collections/associations), RowBounds cannot work correctly because nested rows must be grouped in memory, so skipping/limiting raw rows corrupts grouping. When safeRowBoundsEnabled=true (default false) and a RowBounds with a limit below NO_ROW_LIMIT or offset above 0 is passed to such a statement, ensureNoRowBounds throws.

Source

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

  //
  // HANDLE ROWS FOR SIMPLE RESULTMAP
  //

  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
      RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      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();

View on GitHub (pinned to 008069adb1)

Solutions

  1. Paginate in SQL (LIMIT/OFFSET or ROWNUM) instead of RowBounds for statements with nested resultMaps
  2. If you accept the risk, set <setting name="safeRowBoundsEnabled" value="false"/> to bypass the guard
  3. Use a separate non-nested resultMap (flat DTO + manual grouping) when RowBounds pagination is mandatory

Example fix

// before
List<Order> orders = session.selectList("sel.ordersWithItems", custId,
    new RowBounds(20, 10)); // nested resultMap -> throws when safeRowBounds=true
// after: paginate in SQL
List<Order> orders = session.selectList("sel.ordersWithItemsPaged",
    Map.of("id", custId, "offset", 20, "limit", 10)); // SQL: ... LIMIT #{limit} OFFSET #{offset}
Defensive patterns

Strategy: validation

Validate before calling

// Refuse RowBounds on nested statements when the guard is on
ResultMap rm = session.getConfiguration().getResultMap("orderWithItemsMap");
if (rm.hasNestedResultMaps() && session.getConfiguration().isSafeRowBoundsEnabled()) {
  throw new IllegalArgumentException("Paginate in SQL for nested resultMaps: " + rm.getId());
}

Try / catch

try { session.selectList(stmt, param, new RowBounds(off, lim)); } catch (ExecutorException e) { if (e.getMessage().contains("RowBounds")) { rerun with SQL-level pagination } else throw e; }

Prevention

When it happens

Trigger: handleRowValues on a resultMap with hasNestedResultMaps(), configuration.isSafeRowBoundsEnabled()==true, and a RowBounds whose getLimit() < RowBounds.NO_ROW_LIMIT (Integer.MAX_VALUE) or getOffset() > 0 — i.e. passing new RowBounds(offset, limit) to a nested-mapping query.

Common situations: Enabling safeRowBoundsEnabled globally then reusing RowBounds-based pagination code on JOIN statements with <collection>/<association>; migrating pagination from simple to nested resultMaps without switching the SQL to LIMIT/OFFSET.

Related errors


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