mybatis/mybatis-3 · error · BindingException

method {name} needs either a @ResultMap annotation, a @Resul

Error message

method {name} needs either a @ResultMap annotation, a @ResultType annotation, or a resultType attribute in XML so a ResultHandler can be used as a parameter.

What it means

executeWithResultHandler() requires that MyBatis knows the row type before streaming rows into a ResultHandler. For non-CALLABLE statements, if the first result map's type is void.class (the default placeholder), MyBatis cannot create result objects, so it demands an explicit result type declaration.

Source

Thrown at src/main/java/org/apache/ibatis/binding/MapperMethod.java:127

      result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      result = (long) rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      result = rowCount > 0;
    } else {
      throw new BindingException(
          "Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }

  private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
    if (!StatementType.CALLABLE.equals(ms.getStatementType())
        && void.class.equals(ms.getResultMaps().get(0).getType())) {
      throw new BindingException(
          "method " + command.getName() + " needs either a @ResultMap annotation, a @ResultType annotation,"
              + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
    }
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
    } else {
      sqlSession.select(command.getName(), param, method.extractResultHandler(args));
    }
  }

  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);

View on GitHub (pinned to 008069adb1)

Solutions

  1. Add resultType="com.example.X" (or a resultMap attribute) to the <select> element
  2. Or annotate the mapper method with @ResultMap("...id...") or @ResultType(X.class)

Example fix

<!-- before -->
<select id="processAll">SELECT * FROM users</select>
<!-- after -->
<select id="processAll" resultType="com.example.User">SELECT * FROM users</select>
Defensive patterns

Strategy: validation

Validate before calling

MappedStatement ms = configuration.getMappedStatement("com.example.UserMapper.processAll");
Class<?> rowType = ms.getResultMaps().get(0).getType();
if (rowType == void.class) {
  throw new IllegalStateException("Add resultType/resultMap before using ResultHandler");
}

Prevention

When it happens

Trigger: Declaring 'void processAll(ResultHandler<X> handler)' in a mapper interface where the XML <select> has no resultType/resultMap attribute and no @ResultMap/@ResultType annotation is present; stored procedures (CALLABLE) are exempt because OUT params carry types.

Common situations: Streaming large result sets with ResultHandler and forgetting resultType; annotation-based mappers using @Select without @ResultType.

Related errors


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