mybatis/mybatis-3 · error · BatchExecutorException

${mappedStatementId} (batch index #${index}) failed. ${prior

Error message

${mappedStatementId} (batch index #${index}) failed. ${priorCount} prior sub executor(s) completed successfully, but will be rolled back.

What it means

BatchExecutor.doFlushStatements() executes each batched Statement via executeBatch(). If the driver raises BatchUpdateException, MyBatis builds a message '<statementId> (batch index #N) failed. M prior sub executor(s) completed successfully, but will be rolled back.' and throws BatchExecutorException carrying the BatchUpdateException, the list of successful BatchResults so far, and the failing BatchResult. Batch index is 1-based; 'prior sub executor(s)' counts earlier statements in the same flush.

Source

Thrown at src/main/java/org/apache/ibatis/executor/BatchExecutor.java:147

          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { // issue #141
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter);
            }
          }
          // Close statement to close cursor #1109
          closeStatement(stmt);
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId()).append(" (batch index #").append(i + 1).append(")")
              .append(" failed.");
          if (i > 0) {
            message.append(" ").append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }

}

View on GitHub (pinned to 008069adb1)

Solutions

  1. Read the BatchExecutorException: getBatchUpdateException() holds the driver error (SQLState/error code) identifying the failing row; the message names the mapped statement and 1-based batch index
  2. Fix the data/constraint issue it points to (unique key conflict, null in NOT NULL column, oversized values)
  3. Batch smaller and flush periodically so failures affect fewer rows and are easier to locate
  4. Use ON DUPLICATE KEY UPDATE / MERGE / ON CONFLICT upserts to tolerate duplicates
  5. For per-row error isolation, fall back to non-batched execution or partition the batch to identify the offending row

Example fix

// before
try (SqlSession s = factory.openSession(ExecutorType.BATCH, false)) {
  UserMapper m = s.getMapper(UserMapper.class);
  users.forEach(m::insert);
  s.flushStatements(); // one bad row -> BatchExecutorException, all rolled back
  s.commit();
}

// after: flush in chunks and use upsert to tolerate conflicts
try (SqlSession s = factory.openSession(ExecutorType.BATCH, false)) {
  UserMapper m = s.getMapper(UserMapper.class);
  int i = 0;
  for (User u : users) {
    m.upsert(u); // INSERT ... ON DUPLICATE KEY UPDATE
    if (++i % 500 == 0) s.flushStatements();
  }
  s.flushStatements();
  s.commit();
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate data likely to violate constraints before batching:
Set<String> existing = new HashSet<>(mapper.findKeys(keys));
List<User> insertable = users.stream()
    .filter(u -> !existing.contains(u.key()))
    .collect(Collectors.toList());

Try / catch

try {
  sqlSession.flushStatements();
} catch (BatchExecutorException bee) {
  int failingIndex = bee.getBatchUpdateException() != null ? -1 : -1;
  // bee.getMessage() names statement + 1-based batch index;
  // bee.getBatchUpdateException() carries SQLState/vendor code for the bad row;
  // earlier successful statements are rolled back — session is unusable, open a new one.
  log.error("Batch failed at index {}: {}", bee.getMessage(),
      bee.getBatchUpdateException() == null ? "" : bee.getBatchUpdateException().getMessage());
  throw bee;
}

Prevention

When it happens

Trigger: ExecutorType.BATCH session with multiple statements/flushes; one batch member violates a constraint (duplicate key, FK, NOT NULL, data too long) or times out; driver rejects a batched statement; mixing incompatible statements in one batch; calling flushStatements()/commit when one of the accumulated batches is already broken.

Common situations: Bulk import scripts where a single bad row fails the whole batch; duplicate-key on upsert-less inserts; batching heterogeneous statements where a later one fails, rolling back earlier successful ones (surprising full-batch rollback); MySQL/Oracle driver differences in BatchUpdateException reporting; exceeding max_allowed_packet with huge batches.

Related errors


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