Our entity classes extend from a base resourse class with @Id and @Version, like this:
@MappedSuperclass
public class RR {
private String id;
private Integer jpaVersion;
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "Our_Uuid_Generator")
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Version
public Integer getJpaVersion() {
return jpaVersion;
}
public void setJpaVersion(Integer jpaVersion) {
this.jpaVersion = jpaVersion;
}
}
In some situations we need to create a new entity and call setId() with non-empty value, and save to DB with JpaRepository.save().
This works fine up to spring boot version 3.1.2.
But in spring boot 3.1.3 it seems to treat the entity with non-empty id as an existing entity and fail to save it:
org.springframework.dao.DataIntegrityViolationException: Detached entity with generated id '......' has an uninitialized version value 'null' : ......User.jpa_version
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:286)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:229)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:244)
at jdk.proxy2/jdk.proxy2.$Proxy178.save(Unknown Source)
at our-code.createIfNotExists()
Caused by: org.hibernate.PropertyValueException: Detached entity with generated id '......' has an uninitialized version value 'null' : ......User.jpa_version
at org.hibernate.persister.entity.AbstractEntityPersister.isTransient(AbstractEntityPersister.java:3889)
at org.hibernate.engine.internal.ForeignKeys.isTransient(ForeignKeys.java:294)
at org.hibernate.event.internal.EntityState.getEntityState(EntityState.java:62)
at org.hibernate.event.internal.DefaultPersistEventListener.entityState(DefaultPersistEventListener.java:112)
at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:85)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:77)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:54)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:755)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:739)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:360)
at jdk.proxy2/jdk.proxy2.$Proxy174.persist(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311)
at jdk.proxy2/jdk.proxy2.$Proxy174.persist(Unknown Source)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:617)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:288)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:136)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:120)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:72)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
... 175 common frames omitted
Comment From: quaff
It's likely an regression at hibernate side, please downgrade hibernate.version and try again, you should prepare a pure JPA test case (without spring-boot and spring-data-jpa) and submit to https://hibernate.atlassian.net/ if it works fine.
Comment From: quaff
It's introduced by https://github.com/hibernate/hibernate-orm/commit/ebfaf1c707319a0f0d1f04c70f6b8d52488da2c7 https://hibernate.atlassian.net/browse/HHH-16586
Your ID generator is not assigned but you are call setId manually, I'm not sure such use case is valid, you could try setJpaVersion(0).
Comment From: wilkinsona
Thanks very much, @quaff.
Comment From: fan77830
It's introduced by hibernate/hibernate-orm@ebfaf1c https://hibernate.atlassian.net/browse/HHH-16586 Your ID generator is not
assignedbut you are callsetIdmanually, I'm not sure such use case is valid, you could trysetJpaVersion(0).
The document seem to suggest such case is allowed: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-persistence.saving-entites "Version-Property and Id-Property inspection (default): By default Spring Data JPA inspects first if there is a Version-property of non-primitive type. If there is, the entity is considered new if the value of that property is null. Without such a Version-property Spring Data JPA inspects the identifier property of the given entity. If the identifier property is null, then the entity is assumed to be new. Otherwise, it is assumed to be not new."
Comment From: quaff
It's introduced by hibernate/hibernate-orm@ebfaf1c https://hibernate.atlassian.net/browse/HHH-16586 Your ID generator is not
assignedbut you are callsetIdmanually, I'm not sure such use case is valid, you could trysetJpaVersion(0).The document seem to suggest such case is allowed: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-persistence.saving-entites "Version-Property and Id-Property inspection (default): By default Spring Data JPA inspects first if there is a Version-property of non-primitive type. If there is, the entity is considered new if the value of that property is null. Without such a Version-property Spring Data JPA inspects the identifier property of the given entity. If the identifier property is null, then the entity is assumed to be new. Otherwise, it is assumed to be not new."
First this is the state-detection Strategy from spring-data-jpa, not official JPA Spec, hibernate could act in different behavior.
Second the actual arguable user case is manually assign id to entity which doesn't have an assigned ID generator, from perspective of spring-data-jpa its state is NEW and call of repo.save() will be delegated to em.persist(), It's correct behavior, but from perspective of hibernate, Its state is DETACHED.
org.hibernate.PropertyValueException: Detached entity with generated id '......' has an uninitialized version value 'null' : ......User.jpa_version
Comment From: quaff
package com.example.demo;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
public class TestEntityRepositoryTests {
@Autowired
private TestEntityRepository testEntityRepository;
@ValueSource(booleans = { false, true })
@ParameterizedTest
void test(boolean assigned) {
TestEntity entity = new TestEntity();
if (assigned) {
entity.setId(1L); // will be ignored if hibernate.version <= 6.2.6
}
entity.setName("test");
entity = testEntityRepository.save(entity); // will fail if hibernate.version > 6.2.6
if (assigned) {
assertThat(entity.getId()).isEqualTo(1L); // will fail if hibernate.version <= 6.2.6
}
assertThat(entity.getVersion()).isEqualTo(0);
}
}
package com.example.demo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
@Entity
class TestEntity {
@Id
@GeneratedValue
private Long id;
private String name;
@Version
private Integer version;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
}
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
interface TestEntityRepository extends JpaRepository<TestEntity, Long> {
}
Before hibernate-6.2.7.Final, It seems work fine but the ID assigned manually is ignored, I think it's not what you wanted. You should remove meaningless setId() to fix it. @fan77830
Comment From: kdejaeger
Same issue here.
But if you're using TestEntity in a Resource @Put , so coming from the frontend, you need the setter, right? For example when you modify the 'name' in an input field in a react frontend, and send it over in a put request (including the id field).
Comment From: quaff
Same issue here.
But if you're using TestEntity in a Resource
@Put, so coming from the frontend, you need the setter, right? For example when you modify the 'name' in an input field in a react frontend, and send it over in a put request (including the id field).
I mean remove setId() call, leave id empty for creation.
Comment From: kubav182
Is there any solution? What if I want to upsert entity with ID not null and version null? This should be possible. In such case I don't want ID to be generated by generator.
Comment From: dlehammer
Hi @quaff ,
.. leave id empty for creation.
When creating a Set<TestEntity> of entities, I seem to need to populate the Id attribute in-order to have multiple elements in the Set.
How is that scenario supported? 🤔
Comment From: quaff
What if I want to upsert entity with ID not null and version null?
Update entity without checking version will bypass optimistic lock, why would you do that?
Comment From: quaff
Hi @quaff ,
.. leave id empty for creation.
When creating a
Set<TestEntity>of entities, I seem to need to populate the Id attribute in-order to have multiple elements in the Set. How is that scenario supported? 🤔
I don't understand your use case.
Comment From: kubav182
What if I want to upsert entity with ID not null and version null?
Update entity without checking version will bypass optimistic lock, why would you do that?
I would expect to insert new entity.
Comment From: dlehammer
Hi @quaff ,
Sorry for causing confusion, the TestEntity example class doesn't override equals/hashcode, so it should perform intuitively when added to a Set as this case is identity based 😊
In my case I discovered the entities I've inherited override equals/hashcode, but only for id and version, which made it impossible to add multiple of these entities to a Set without overwriting each other, without populating the id.
Which seems to align with Programmatically Managed Primary Key case.
(currently I'm still stuck with the Detached entity with generated id '...' has an uninitialized version value 'null', even when I populate the version to 0 before handing of to the entity-manager 😕 )
Comment From: quaff
What if I want to upsert entity with ID not null and version null?
Update entity without checking version will bypass optimistic lock, why would you do that?
I would expect to insert new entity.
Spring Data JPA will treat such entity as isNew, and dispatch to entityManager.persist(), then insert happens, if it doesn't work please provide a minimal reproducer.
Comment From: quaff
currently I'm still stuck with the
Detached entity with generated id '...' has an uninitialized version value 'null', even when I populate theversionto 0 before handing of to the entity-manager
@dlehammer Do you expect insert or update? I would like to take a look if you provide a minimal reproducer.
Comment From: dlehammer
Hi @quaff,
Thank you for taking the time to respond.
Do you expect insert or update?
I would expect merge + flush to perform an insert for a fully populated entity that hasn't been persisted prior, as I understand it the EntityManager should perform a lookup when merging to determine the persisted state (if any).
I would like to take a look if you provide a minimal reproducer.
Thanks a bunch for the kind offer 🙏
But I ended up "returning to basics" by extending the entity equals/hashcode method to include sufficient attributes instead, in-order to allow populating the Set with multiple unpersisted instances.
This approach proved the least intrusive and allowed the persisting of the parent -> Set of children -> Set of child metadata via the parent JPA repository.
I ended up with this result, as any attempt permutation where I tried to merge entities resulted in constraint violations during parent persisting.
Comment From: kolotushkina
@fan77830 Hello, did you find a solution for your case without downgrading Spring boot?