hibernate/hibernate-orm · error · StaleObjectStateException

Row was already updated or deleted by another transaction

Error message

Row was already updated or deleted by another transaction

What it means

Error "Row was already updated or deleted by another transaction" thrown in hibernate/hibernate-orm.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java:453

		// We must clone embedded composite identifiers, or we will get
		// back the same instance that we pass in. Apply the special MERGE
		// fetch profile and perform the resolution (Session#get).
		final Object result =
				session.getLoadQueryInfluencers()
						.fromInternalFetchProfile( CascadingFetchProfile.MERGE,
								() -> session.find( entityName, clonedIdentifier ) );

		if ( result == null ) {
			EVENT_LISTENER_LOGGER.detachedInstanceNotFoundInDatabase();
			// we got here because we assumed that an instance
			// with an assigned id and no version was detached
			// when it was really transient (or deleted)
			final Boolean knownTransient = persister.isTransient( entity, session );
			if ( knownTransient == Boolean.FALSE ) {
				// we know for sure it's detached (generated id
				// or a version property), and so the instance
				// must have been deleted by another transaction
				throw new StaleObjectStateException( entityName, id );
			}
			else {
				// we know for sure it's transient, or we just
				// don't have information (assigned id and no
				// version property) so keep assuming transient
				entityIsTransient( event, clonedIdentifier, copyCache );
			}
		}
		else {
			// before cascade!
			copyCache.put( entity, result, true );
			final Object target = targetEntity( event, entity, persister, id, result );
			// cascade first, so that all unsaved objects get their
			// copy created before we actually copy
			cascadeOnMerge( session, persister, entity, copyCache );

			final var interceptor = session.getInterceptor();
			final String[] propertyNames = persister.getPropertyNames();

View on GitHub (pinned to fad1729dce)

Solutions

  1. Use optimistic locking (@Version) and catch OptimisticLockException to retry with fresh data.
  2. Reload the entity before merging when concurrent updates are expected.

When it happens

Trigger: Thrown when a flush detects that the database row was concurrently modified or removed by another transaction (stale optimistic state).

Common situations: Typical situations: concurrent transactions updating the same row; long-running transactions with optimistic locking; reusing stale entities across transactions.


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