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
- 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
- Fix the data/constraint issue it points to (unique key conflict, null in NOT NULL column, oversized values)
- Batch smaller and flush periodically so failures affect fewer rows and are easier to locate
- Use ON DUPLICATE KEY UPDATE / MERGE / ON CONFLICT upserts to tolerate duplicates
- 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
- Flush every N rows so failures are cheap and localized
- Clean/dedupe data before import; prefer upsert statements for idempotent loads
- Log the mapped statement id and batch index from BatchExecutorException to pinpoint the row
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
- Error: Cannot rollback. No managed session is started.
- Unknown execution method for: {name}
- Mapper method '{name}' attempted to return null from a metho
- Mapper method '{name}' has an unsupported return type: {retu
- method {name} needs either a @ResultMap annotation, a @Resul
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/cce0db4f0f250aa3.
Report an issue: GitHub.