OptimisticLockException when manually setting the ID for the entity

Hello,

In the Hibernate 6.5 version, we got a new behavior.
When we set the ID manually for the entity we got this exception:

jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

I found the explanation for this behavior here:
6.6 Migration Guide (Merge versioned entity when row is deleted)

Actually, we set ID manually mainly in the Tests in our application. That code has worked well for many years and for now we need totally rework it.

The questions are:

  1. Is there some way to avoid that OptimisticLockException exception when we sitting ID manually at least for tests?
  2. Possibly we configured something wrong or my understanding of that behavior is incorrect?

Thank you!

This was a deliberate decision. I understand that testing might get more complicated, but you really shouldn’t rely on fixed id values if you have a generator. This can lead to lot’s of other problems.

I think if you’re using Quarkus, Spring or other containers, you should be able to create a class like this and mark it as a bean to make this work:

	public class SequenceOrAssignedGenerator extends SequenceStyleGenerator {
		@Override
		public Object generate(SharedSessionContractImplementor session, Object owner) throws HibernateException {
			final Long id;
			if ( owner instanceof MyEntity ) {
				id = ( (MyEntity) owner ).getId();
			}
			else {
				id = null;
			}

			return id != null ? id : super.generate( session, owner );
		}

		@Override
		public boolean allowAssignedIdentifiers() {
			return true;
		}
	}

Please let us know if it does. Maybe you can even contribute a section in the migration guide for ORM 6.6 that explains this “workaround”?

This can lead to lot’s of other problems.

Sure. We understand this. But we have some legacy code in the older version of our application and we don’t want to invest so much in that direction.

Please let us know if it does

Thanks! This magic property works for us.
We already had a custom IdGenerator with the logic that you have shared in the overridden generate method but without allowAssignedIdentifiers property it did not work.

Maybe you can even contribute a section in the migration guide for ORM 6.6 that explains this “workaround”?

Is there some link on how to do this?

Sure, here is the migration guide on GitHub. Here some details to getting started.

1 Like

Doesn’t work in spring as you claim. Adding @Component to this doesn’t do squat.
Don’t change this part of hibernate every few versions. Every time this hits us, since you never bother to document such core changes.

This was a deliberate decision.
With which, you f****d over 100’s of projects. You knew this would cause issues, since you said this was deliberate. So you knew this. And yet, this isn’t documented in the migration guide.
Do I really need to throw up the debugger again to reverse engineer how this works, so I can override the SequenceStyleGenerator? I’m so fed up with this. Every damn few versions this part get’s broken without documenting it.
You should sometimes listen to Linus Torvalds. We don’t break user-space.
Why. You knew you would break test setups. If at least, your suggested way worked, but the generator doesn’t get triggered.

I would suggest you adapt your tone a bit unless you want to be blocked.

In case you didn’t see this when you typed the URL or looked at the logos of this forum, this is a Hibernate ORM forum, not a Spring forum. Maintainers of Hibernate projects don’t develop Spring or decide to what version it updates to, so the suggestions we give for Spring here are a best effort.

We have tests that show replacing the generator works fine and the OP even accepted the answer as solution, so maybe the problem is your setup.
If you think that there is a bug when declaring it as a bean, please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

This is an open source project, so if you think this is important to mention, the please go ahead and propose a PR to mention it.

The thing is, this was never documented to work. It just worked by coincidence. We acknowledged that the behavior is useful for tests, which is why we added this allowAssignedIdentifiers method through an improvement ticket so you can override the behavior and even wrote about it in the ORM 6.5 release announcement. Since it is dangerous to use in production, this is not enabled by default and requires explicit code changes.

Adding @Component to this doesn’t do squat

You don’t need to add it as a @Component.
You should override generate() method from the parent Generator and then refer to it from the entity.

Before 6.5 version it looked like this:

    @GenericGenerator(
            .....
            type = YourCustomGenerator.class
            .... )

I agree this is an ok decision to make. We just stumbled upon the actual exception - it feels very misleading to me that this is an Locking Exception because its plainly not. This exception is deliberately thrown because the id is expected to be generated by the DB but is already set on a detached entity. I would suggest there should be an explicit exception for this case (something along the lines of “GeneratedIdAlreadySetException”) with an explicit message for this case. We went on a wild hunt because we assumed there was some wild things going on regarding transaction etc.

To be fair, Hibernate ORM has no way of knowing what scenario this is. It could just as well be a detached entity that was already removed by a concurrent transaction.

Is the exception actually telling the thruth in the scenario you are mentioning? It would be not my first guess if I see an “OptimistLockException” in either of the 2 cases.
I feel like both scenarios would profit from a more telling exception and/or message.

Just for completeness:
Our scenario is actually that we map Entities into domain objects and vice versa, so the entity is not managed when getting updated, but the id is obviously set

I don’t know what kind of message you think would be more helpful in this scenario. If you merge an entity object that has its id field set, which is also marked as generated, then Hibernate ORM must assume that this object exists and hence throw an error when a find/update fails. This is actually mentioned in the JPA spec as pointed out in the migration guide. Clearly, if the row was removed in the meantime, this is an optimistic lock exception.

I see, thanks for the explanation :slight_smile:
IMO, this change actually introduced a second error case, but the exception only mentions one. This “new” error is not a database integrity error, its rather a configuration issue (assuming the code is valid). A hint of any form would be appreciated, especially considering it worked before (which I agree was wrong).
I see that Hbernate cannot differentiate between the two scenarios, but why not tell the progammer exactly that?

Again, we’re open to improvement suggestions. Please propose a PR.
It is a programmer error though to set the id value of an entity when it is marked as @GeneratedValue, unless you know a row for that object should exist.

You can create own annotation that will be used in old behavior Java + Spring:

/**
 * Copy all SequenceGenerator params
 */
@IdGeneratorType(LegacySequenceGeneratorImpl.class)
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface LegacySequenceGenerator {
    String sequenceName() default "";
    String schema() default "";
    int initialValue() default 1;
    int allocationSize() default 50;
    String optimizer() default "pooled-lo";
}
/**
 * Use default behavior for custom generator as SequenceGenerator annotation.
 */
public class LegacySequenceGeneratorImpl extends SequenceStyleGenerator {
    
    private final LegacySequenceGenerator annotation;

    public LegacySequenceGeneratorImpl(LegacySequenceGenerator annotation) {
        this.annotation = annotation;
    }
    
    /**
     * Pass custom annotation settings to SequenceStyleGenerator
     */
    @Override
    public void configure(Type type, Properties parameters, ServiceRegistry serviceRegistry) throws MappingException {
        if (parameters == null) {
            parameters = new Properties();
        }

        String fullSequenceName;
        if (!annotation.schema().isBlank()) {
            parameters.put("schema", annotation.schema());
            fullSequenceName = annotation.schema() + "." + annotation.sequenceName();
        } else  {
            fullSequenceName = annotation.sequenceName();
        }

        parameters.put("sequence_name", fullSequenceName);
        parameters.put("initial_value", annotation.initialValue());
        parameters.put("increment_size", annotation.allocationSize());
        parameters.put("optimizer", annotation.optimizer());

        super.configure(type, parameters, serviceRegistry);
    }

    /**
     * This is important to return old behavior
     */
    @Override
    public boolean allowAssignedIdentifiers() {
        return true;
    }
}

And usages:

@Entity
public class UserEntity {

    @Id
    @LegacySequenceGenerator(sequenceName = "user_id_sqn")
    private Integer id;

}

It can help for migration to new Spring version, but in the future it will be necessary to fix all the places where it is used.