Manually setting the identifier results in object optimistic locking failure exception

In a Spring Boot application, for a particular use case (data export & import), we would like to preserve the identifiers for objects that are otherwise assigned automatically by @GeneratedValue(strategy = GenerationType.IDENTITY). To achieve that, we were able to just set the ID manually, and Hibernate would create the object in the PostgreSQL database with that identifier.

This was working before 6.6, but now triggers the exception:

org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

@Getter
@Setter
@Entity
public class TestObject {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String name;
}
public class TestObjectRepositoryTest {
	@Inject
	private TestObjectRepository repository;

	@Test
	public void addObjectWithoutId() {
		var object = new TestObject();
		object.setName("test");
		repository.save(object);
	}

	@Test
	public void addObjectWithId() {
		var object = new TestObject();
		object.setId(1234L);
		object.setName("test2");
		repository.save(object);
	}
}

The first test passes, but the second one fails.

What are my options to set the ID for these special cases? I guess I could bring up another entity class without the @GeneratedValue annotation for this purpose, potentially with inheritance, but maybe there is a better way?

I have the same problem, but I am using the custom @ IdGeneratiorType method.
I found that in version 6.6, the following judgment has been added. It may be the problem caused by this.

//org.hibernate.event.internal.DefaultMergeEventListener
	protected void entityIsDetached(MergeEvent event, Object copiedId, Object originalId, MergeContext copyCache) {
		......

		if ( result == null ) {
			LOG.trace( "Detached instance not found in database" );
			// 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, source );
			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( source, persister, entity, copyCache );
			copyValues( persister, entity, target, source, copyCache );
			//copyValues works by reflection, so explicitly mark the entity instance dirty
			markInterceptorDirty( entity, target );
			event.setResult( result );
		}
	}

You cannot manually set generated identifier values using the default identifiers. Thanks to a recent improvement Jira, you can however define a custom generator implementation that overrides the allowAssignedIdentifiers(), returning true, to allow for a custom implementation to sporadically use pre-assigned identifier values.

You can also extend from the existing org.hibernate.id.IdentityGenerator to inherit its default behavior when not using existing identifier values. An example of this can be found here.

2 Likes

Thank you very much, it has been resolved.

Yes, that’s it, thank you very much.