mybatis/mybatis-3 · error · ExecutorException

Nested lazy loaded result property '" + property + "' for qu

Error message

Nested lazy loaded result property '" + property + "' for query id '" + resultLoader.mappedStatement.getId() + " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map."

What it means

ResultLoaderMap keys pending lazy loads by the LEFTMOST segment of the property path (order.customer -> 'order'). Two lazy properties in one resultMap that share the same leftmost segment would fight over the same loader slot and overwrite each other, so addLoader rejects the second one with this ExecutorException. Note the message contains a known typo ('query id ... already exists' string concatenation).

Source

Thrown at src/main/java/org/apache/ibatis/executor/loader/ResultLoaderMap.java:55

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

/**
 * @author Clinton Begin
 * @author Franta Mejta
 */
public class ResultLoaderMap {

  private final Map<String, LoadPair> loaderMap = new HashMap<>();

  public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
    String upperFirst = getUppercaseFirstProperty(property);
    if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
      throw new ExecutorException("Nested lazy loaded result property '" + property + "' for query id '"
          + resultLoader.mappedStatement.getId()
          + " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
    }
    loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
  }

  public final Map<String, LoadPair> getProperties() {
    return new HashMap<>(this.loaderMap);
  }

  public Set<String> getPropertyNames() {
    return loaderMap.keySet();
  }

  public int size() {
    return loaderMap.size();
  }

View on GitHub (pinned to 008069adb1)

Solutions

  1. Give the two lazy properties distinct leftmost segments, e.g. property="pendingOrders" and property="shippedOrders"
  2. Combine them into a single nested select that populates the shared parent in one query

Example fix

<!-- before -->
<resultMap id="m" type="T">
  <association property="orders.pending" select="selPending" fetchType="lazy"/>
  <association property="orders.shipped" select="selShipped" fetchType="lazy"/>
</resultMap>

<!-- after -->
<resultMap id="m" type="T">
  <association property="pendingOrders" select="selPending" fetchType="lazy"/>
  <association property="shippedOrders" select="selShipped" fetchType="lazy"/>
</resultMap>
Defensive patterns

Strategy: validation

Validate before calling

// Lint resultMaps: no two lazy properties may share a leftmost segment
Set<String> leftmost = new HashSet<>();
for (String prop : lazyPropertyPaths) {
  String head = prop.split("\\.")[0];
  if (!leftmost.add(head)) throw new IllegalStateException("duplicate leftmost lazy property: " + head);
}

Prevention

When it happens

Trigger: A resultMap declaring two nested selects whose property paths start with the same segment, e.g. property="orders.pending" and property="orders.shipped" — both map to key 'orders'.

Common situations: Splitting one association into multiple lazy sub-properties under the same parent name; refactoring resultMaps during entity redesign.

Related errors


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