spring data jpa many to many relationship reports a missed entity passed to persist error

Keywords: Programming Java Hibernate Database Spring

Recently, when spring data jpa is used to maintain many to many relationships between tables, a strange problem arises. When a new object is put into the entity class to maintain many to many relationship set, and then saved with jpa, the following exception will appear.

org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.infiai.webmessenger.dao.ProductTag; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.infiai.webmessenger.dao.ProductTag
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:299)
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244)
        at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:488)
        at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59)
        at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:147)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:57)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
        at com.sun.proxy.$Proxy127.saveAndFlush(Unknown Source)
        at com.infiai.webmessenger.service.UserService.addUser(UserService.java:38)
        at com.infiai.webmessenger.task.UpdateM3OrderDataTask.updateM3OrderDataTask(UpdateM3OrderDataTask.java:128)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65)
        at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
        at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.infiai.webmessenger.dao.ProductTag
        at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:124)
        at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:765)
        at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:758)
        at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:80)
        at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:398)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:323)
        at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:162)
        at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:431)
        at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:363)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:326)
        at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:162)
        at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:111)
        at org.hibernate.event.internal.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:456)
        at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:278)
        at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:178)
        at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:109)
        at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:67)
        at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:189)
        at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:132)
        at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:58)
        at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:775)
        at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:748)
        at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:753)
        at org.hibernate.jpa.spi.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:1146)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298)
        at com.sun.proxy.$Proxy116.persist(Unknown Source)
        at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:508)
        at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:522)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:504)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:489)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:461)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:56)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)

Use jpa to maintain a bidirectional many to many relationship between entity classes User and ProductTag as follows

@Entity
@Table(name = "user",
indexes = {@Index(name="unique_index", columnList="name,psid", unique = true)})
public class User
{
	@Id
	@GeneratedValue
	private Integer id;
	private String name;
	private String psid;
	@ManyToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
	@JoinTable(name = "user_tag", 
				joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
				inverseJoinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id")}
	)
	private Set<ProductTag> tagSet = new HashSet<ProductTag>();
    
    //get set method omitted

}

@Entity
@Table(name = "product_tag",
indexes = {@Index(name="unique_index", columnList="tagName", unique = true)})
public class ProductTag
{
	@Id
	@GeneratedValue
	private Integer id;
	private String tagName;
	@ManyToMany(mappedBy = "tagSet", fetch=FetchType.EAGER)
	private Set<User> userSet = new HashSet<User>();

    //get set method omitted
}

Using the following method to insert data into the database will report a detached entity passed to persist error

User user = userService.findByNameAndPsid(orderData.getClientName(), orderData.getPsid());
tag = new ProductTag();
tag.setTagName(data.getAsin());
user.getTagSet.add(tag);
userService.addUser(user);

The user object is queried from the database by jpa, so it is persistent. The tag is a free object.

Adding the free state object to the set set set where the user maintains the many to many relationship, and then saving it will cause the above error.

After analysis, there are two main reasons for this error:

1. The tag object is a free state object, which needs to be added to the database first to become a persistent state, and then added to the tagSet of the user object. So it will report the error of free state.

2. Because the entity class is a two-way many to many relationship, it needs to maintain the many to many relationship in both parties, user and productTag. Therefore, users need to be added to the userSet collection of productTag.

After modification, the following code was saved successfully

@Entity
@Table(name = "user",
indexes = {@Index(name="unique_index", columnList="name,psid", unique = true)})
public class User
{
	@Id
	@GeneratedValue
	private Integer id;
	private String name;
	private String psid;
	@ManyToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
	@JoinTable(name = "user_tag", 
				joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
				inverseJoinColumns = {@JoinColumn(name = "tag_id", referencedColumnName = "id")}
	)
	private Set<ProductTag> tagSet = new HashSet<ProductTag>();
	
	public void addTag(ProductTag tag, boolean setTag)
	{
		tagSet.add(tag);
		if(setTag)
		{
			tag.addUser(this, false);
		}
	}

    //Ignore get set method
}



@Entity
@Table(name = "product_tag",
indexes = {@Index(name="unique_index", columnList="tagName", unique = true)})
public class ProductTag
{
	@Id
	@GeneratedValue
	private Integer id;
	private String tagName;
	@ManyToMany(mappedBy = "tagSet", fetch=FetchType.EAGER)
	private Set<User> userSet = new HashSet<User>();
	
	public void addUser(User user, boolean setUser)
	{
		userSet.add(user);
		if(setUser)
		{
			user.addTag(this, false);
		}
	}
    //Ignore get set method

}

 

User user = userService.findByNameAndPsid(orderData.getClientName(), orderData.getPsid());
tag = new ProductTag();
tag.setTagName(data.getAsin());
//Save the free object first
tag = tagService.addTag(tag);
user.addTag(tag, true);
userService.addUser(user);

The addUser method maintains the many to many relationship between the two ends, and the addTag method is the same. The boolean variable setuser and settag are used to prevent infinite loop storage.

Finally, it is concluded that the reason for this problem is that two-way entity class relationships need to be maintained at both ends. When saving many to many relationships, both sides need to be persistent.

Entity class many to many relationships and database consistency are discussed in detail in the following links

    https://stackoverflow.com/questions/13370221/jpa-hibernate-detached-entity-passed-to-persist

    https://notesonjava.wordpress.com/2008/11/03/managing-the-bidirectional-relationship/

Posted by Shadow Wolf on Fri, 17 Jan 2020 00:58:31 -0800