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
- If the SQL returns all rows of a parent contiguously (ordered by parent key), set resultOrdered="true" on the <select> to declare safety
- Otherwise use the default handler (selectList) for nested-mapped statements and post-process the returned graph
- 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
- Mark ordered JOIN statements resultOrdered=true with a matching ORDER BY
- Route nested-mapped queries through selectList unless ordering is proven
- Cover ResultHandler code paths in integration tests
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
- Mapped Statements with nested result mappings cannot be safe
- Invalid bound statement (not found): {mapperInterface}.{meth
- Type {type} is not known to the MapperRegistry.
- Error resolving JdbcType. Cause: {cause}
- Error resolving ResultSetType. Cause: {cause}
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/5a0ef84b71dfd598.
Report an issue: GitHub.