hibernate/hibernate-orm · error · AuditException

Cannot update previous revision for entity %s and id %s (%s

Error message

Cannot update previous revision for entity %s and id %s (%s rows modified).

What it means

At transaction end the audit coordinator 'closes' the previous revision by updating it; verifyTransactionEndOutcome expects exactly one affected row (zero tolerated only for ModificationType.ADD). AuditException reports either more than one matched row (multiple revision rows matched the close predicate) or zero rows for a non-ADD change (the revision row that should exist is gone). Both indicate the audit rows were changed or removed outside the coordinator's assumptions.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityAuditSupport.java:437

		}

		sourceMappings[tableIndex].getKeyMapping().breakDownKeyJdbcValues(
				id,
				(jdbcValue, columnMapping) -> jdbcValueBindings.bindValue(
						jdbcValue, columnMapping.getColumnName(), ParameterUsage.RESTRICT
				),
				session
		);
	}

	public static boolean verifyTransactionEndOutcome(
			int affectedRowCount,
			ModificationType modificationType,
			String entityName,
			Object id) {
		if ( affectedRowCount > 1
				|| affectedRowCount == 0 && modificationType != ModificationType.ADD ) {
			throw new AuditException(
					"Cannot update previous revision for entity "
							+ entityName + " and id " + id
							+ " (" + affectedRowCount + " rows modified)."
			);
		}
		return true;
	}

	private EntityTableMapping[] buildAuditTableMappings() {
		final EntityTableMapping[] sourceMappings = entityPersister.getTableMappings();
		final EntityTableMapping[] result = new EntityTableMapping[sourceMappings.length];
		for ( int i = 0; i < sourceMappings.length; i++ ) {
			final EntityTableMapping source = sourceMappings[i];
			if ( source.isInverse() ) {
				continue;
			}
			final String auditTableName = auditMapping.resolveTableName( source.getTableName() );
			result[i] = createAuxiliaryTableMapping( source, entityPersister, auditTableName );

View on GitHub (pinned to fad1729dce)

Solutions

  1. Stop external deletion/modification of audit rows while transactions run - archive via a controlled export instead
  2. Treat it like an optimistic-lock failure: rollback and retry the unit of work
  3. Serialize updates to hot entities (e.g. pessimistic lock on the entity) so revision close never races
  4. Inspect the audit table for duplicate or missing revision rows for the reported entity/id and repair the data
Defensive patterns

Strategy: retry

Try / catch

for (int attempt = 0; attempt < 3; attempt++) {
    try {
        tx = begin(); doWork(); tx.commit(); break;
    } catch (AuditException e) {
        rollback();
        if (!e.getMessage().contains("rows modified")) throw e;
        backoff(attempt); // concurrent revision close: retry the unit of work
    }
}

Prevention

When it happens

Trigger: Concurrent transactions revising the same entity + id so a revision-close update matches several rows; audit revision rows deleted or modified by external jobs, scripts, or DBAs while the application runs; test runs against a dirty audit schema with leftover revision rows.

Common situations: Retention/archival jobs purging audit rows live; hot entities updated concurrently by users and batch jobs; environments where audit tables are hand-edited for corrections.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/4fa1ed4237e157ed. Report an issue: GitHub.