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
- Paginate in SQL (LIMIT/OFFSET or ROWNUM) instead of RowBounds for statements with nested resultMaps
- If you accept the risk, set <setting name="safeRowBoundsEnabled" value="false"/> to bypass the guard
- 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
- Prefer SQL LIMIT/OFFSET pagination everywhere
- If using RowBounds, keep nested resultMaps off those statements
- Document why safeRowBoundsEnabled is set if anyone disables it
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
- Mapped Statements with nested result mappings cannot be safe
- Invalid bound statement (not found): {mapperInterface}.{meth
- {methodName} cannot have multiple {paramType} parameters
- Type {type} is not known to the MapperRegistry.
- Error resolving JdbcType. Cause: {cause}
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/cf2eedf02d2d6f9e.
Report an issue: GitHub.