hibernate/hibernate-orm · error · StaleObjectStateException

Persistence context contains a more recent version of the gi

Error message

Persistence context contains a more recent version of the given entity

What it means

While deleting a detached entity, flushAndEvictExistingEntity() finds a managed instance under the same key in the persistence context, flushes, and compares versions: the managed copy's version differs, so the detached copy you passed is stale. Hibernate throws StaleObjectStateException('Persistence context contains a more recent version of the given entity') — optimistic locking applied on the remove path.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java:236

			@Nonnull EventSource source) {
		final var persistenceContext = source.getPersistenceContextInternal();
		final Object existingEntity = persistenceContext.getEntity( key );
		if ( existingEntity != null ) {
			if ( persistenceContext.getEntry( existingEntity ).getStatus().isDeletedOrGone() ) {
				// already deleted, no work to do
				return true;
			}
			else {
				EVENT_LISTENER_LOGGER.flushAndEvictOnRemove( key.getEntityName() );
				source.flush();
				if ( !persister.isVersioned()
						|| persister.getVersionType()
								.isEqual( version, persister.getVersion( existingEntity ) ) ) {
					source.evict( existingEntity );
					return false;
				}
				else {
					throw new StaleObjectStateException( key.getEntityName(), key.getIdentifier(),
							"Persistence context contains a more recent version of the given entity" );
				}
			}
		}
		else {
			return false;
		}
	}

	private void deletePersistentInstance(
			@Nonnull DeleteEvent event,
			@Nonnull DeleteContext transientEntities,
			@Nonnull Object entity,
			@Nonnull EntityEntry entityEntry) {
		EVENT_LISTENER_LOGGER.deletingPersistentInstance();
		final var source = event.getSession();
		if ( entityEntry.getStatus().isDeletedOrGone()
				|| source.getPersistenceContextInternal()

View on GitHub (pinned to fad1729dce)

Solutions

  1. Use merge() before remove so the version is reconciled against the managed copy
  2. Re-fetch fresh state in the current session and delete that: session.remove(session.find(X.class, id))
  3. Catch StaleObjectStateException and either retry the unit of work from fresh state or report a conflict
  4. Avoid carrying detached instances across long conversations without version checks

Example fix

// before
session.remove(staleOrder); // session manages a newer version -> StaleObjectStateException

// after
Order fresh = session.find(Order.class, staleOrder.getId());
session.remove(fresh);
Defensive patterns

Strategy: retry

Validate before calling

// avoid the stale path: delete the managed, most recent version
Order fresh = session.find(Order.class, detached.getId());
if (fresh != null) {
    session.remove(fresh);
}

Try / catch

try {
    session.remove(detachedOrder);
} catch (StaleObjectStateException e) {
    // persistence context has a newer version: reload fresh state and retry once
    Order fresh = session.find(Order.class, detachedOrder.getId());
    session.remove(fresh);
}

Prevention

When it happens

Trigger: session.remove(detachedEntity) while the same session manages a newer version of that row (loaded after your detached copy was read); a detached instance built from stale state mixed back into an active session; two loads of the same row in one conversation with a bump in between.

Common situations: Long conversations with detached reattachment; delete flows fed by cached/stale frontend state while background jobs bumped the version; optimistic-lock @Version columns added to entities used in delete paths.

Related errors


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