Overview:
I'm building a spring-boot application which, in part, retrieves some entities from an external REST service and compares it to previous versions of the entity held locally in a database.
I'm injecting EntityManager
with @PersistenceContext
, and using that to work with the database, as there are many entity types, and the type is initially unknown to the module. I could get a JpaRepository
from a factory, but the number of different entity types is liable to grow, and I'd rather not rely on that if at all possible.
Problem:
When the module retrieves an entity which it doesn't hold in the database, it does some business logic and then tries to persist the new entity.
The Person
class, which is one of the entities in question, contains three fields of type Site
, which often hold the same objects.
When I try to persist a new Person
which has the same Site
object in multiple fields with CascadeType.PERSIST
, I get an EntityExistsException
(see stacktrace (1)).
When I remove the CascadeType.PERSIST from the Site
fields, and try to persist a new Person
which has the same Site
object in multiple fields, I get a TransientPropertyValueException
(see stacktrace (2)).
I think I understand the reasons why both exceptions occur:
In the first case it's because after the first site field is cascade-persisted, it cannot be repersisted for the second field.
The second case I think is because the the @Transactional
annotation is trying to flush the transaction without the site instance(s) being persisted.
I've tried removing the @Transactional
annotation and beginning and commiting an EntityTransaction myself, but I get an IllegalStateException
(see stacktrace (3)), though I think this is expected as spring should be handling the transactions itself.
I've looked at answers to similar questions (e.g. this, this) but all suggest some variation of changing the CascadeType.
In another question someone suggested to make sure that the entities in question were being evaluated correctly by the equals()
method, so I checked in the debugger, and ((Person)newEntity).currentSite.equals(((Person)newEntity).homeSite)
evaluates to true.
How can I go about consistently persisting/merging entities with the same object accross multiple fields?
Edit: I've also tried the various combinations of cascade types with fetch = FetchType.EAGER
, but this produces no change in their respective exceptions.
Edit 2: I've tried using a JpaRepository
instead of using the EntityManager
, and get effectively the same set of exceptions depending on what cascade types I'm using.
If I use PERSIST
, but no MERGE
, I get an EntityNotFoundException
(see stacktrace (4)), and if I use both PERSIST
and MERGE
I get an InvalidDataAccessApiUsageException` (see stacktrace (5)).
Person:
@EqualsAndHashCode(callSuper = true)
@javax.persistence.Entity
@XmlDiscriminatorValue("person")
@XmlRootElement(name = "person")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({Subscriber.class})
public class Person extends MobileResource implements Serializable {
private static final Logger LOG = LogManager.getLogger(Person.class);
private String firstName;
private String surname;
public Person() {
super();
}
public Person(Long id) {
super(id);
}
public Person(Person that) {
super(that);
this.firstName = that.firstName;
this.surname = that.surname;
}
// getters && setters
}
MobileResource:
@EqualsAndHashCode(callSuper = true)
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@XmlRootElement(name = "resource")
@XmlDiscriminatorNode("@type")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({Vehicle.class, Person.class})
public abstract class MobileResource extends Resource implements Serializable {
private static final Logger LOG = LogManager.getLogger(MobileResource.class);
@ManyToOne(cascade = CascadeType.ALL)
private MobileResourceStatus status;
private Long incidentId;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private Site homeSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private Site currentSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private Site relocationSite;
public MobileResource() {
super();
}
public MobileResource(Long id) {
super(id);
}
public MobileResource(MobileResource that) {
super(that);
this.status = that.status;
this.incidentId = that.incidentId;
this.homeSite = that.homeSite;
this.currentSite = that.currentSite;
this.relocationSite = that.relocationSite;
}
// getters && setters
}
Site:
@EqualsAndHashCode(callSuper = true)
@javax.persistence.Entity
public class Site extends Resource implements Serializable {
private static final Logger LOG = LogManager.getLogger(Site.class);
private String location;
public Site() {
super();
}
public Site(Long id) {
super(id);
}
public Site(Site that) {
super(that);
this.location = that.location;
}
}
Resource:
@EqualsAndHashCode
@MappedSuperclass
@XmlRootElement
@XmlSeeAlso({MobileResource.class})
public abstract class Resource implements Entity, Serializable {
private static final Logger LOG = LogManager.getLogger(Resource.class);
@Id
private Long id;
private String callSign;
@XmlPath(".")
private LatLon latLon;
private Long brigadeId;
private Long batchId;
@ManyToMany(cascade = CascadeType.ALL)
private List<Attribute> attributes;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private ResourceType type;
public Resource() {
}
public Resource(Long id) {
this.id = id;
}
public Resource(Resource that) {
this.id = that.id;
this.callSign = that.callSign;
this.latLon = that.latLon;
this.attributes = that.attributes;
this.batchId = that.batchId;
this.brigadeId = that.brigadeId;
this.type = that.type;
}
// getters && setters
}
DefaultEntityMessageHandler:
@Component
public class DefaultEntityMessageHandler implements EntityMessageHandler {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public void handleEntityMessage(EntityMessageData data, Message message) {
// business logic
if (newEntity != null) {
if (oldEntity != null)
entityManager.merge(newEntity);
else
entityManager.persist(newEntity);
}
}
}
Stacktrace (1):
2018-06-06 12:05:15,975 ERROR ActiveMQMessageConsumer - ID:cpt-9225-1528283097161-1:1:1:1 Exception while processing message: ID:cpt-8919-1528281875592-1:1:1:1:4
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [my.class.path.entity.resource.site.Site#738]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:118)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:164)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:813)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:773)
at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:80)
at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:467)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:392)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:193)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:126)
at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:414)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:252)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:182)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:125)
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)
...
Changing the cascade type in MobileResource:
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE, CascadeType.DETACH})
private Site homeSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE, CascadeType.DETACH})
private Site currentSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE, CascadeType.DETACH})
Stacktrace (2):
2018-06-06 12:19:24,084 ERROR ExceptionMapperStandardImpl - HHH000346: Error during managed flush [org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : my.class.path.entity.resource.mobile_resource.person.Person.currentSite -> my.class.path.entity.resource.site.Site]
2018-06-06 12:19:24,093 ERROR ActiveMQMessageConsumer - ID:cpt-9436-1528283955454-1:1:1:1 Exception while processing message: ID:cpt-8919-1528281875592-1:1:1:1:8
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : my.class.path.entity.resource.mobile_resource.person.Person.currentSite -> my.class.path.entity.resource.site.Site; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : my.class.path.entity.resource.mobile_resource.person.Person.currentSite -> my.class.path.entity.resource.site.Site
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:365)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:540)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
...
Stacktrace (3):
2018-06-06 13:29:35,594 ERROR ActiveMQMessageConsumer - ID:cpt-9864-1528288166188-1:1:1:1 Exception while processing message: ID:cpt-8919-1528281875592-1:1:1:1:9
java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:254)
at com.sun.proxy.$Proxy114.getTransaction(Unknown Source)
at my.class.path.entity_controller.DefaultEntityMessageHandler.handleEntityMessage(DefaultEntityMessageHandler.java:60)
at my.class.path.entity_listener.listeners.IdExtractorMessageListener.onMessage(IdExtractorMessageListener.java:41)
...
Stacktrace (4)
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-06-06 15:26:36,143 ERROR SpringApplication - Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:793)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1234)
at my.class.path.OfficerSubscription.main(OfficerSubscription.java:44)
Caused by: org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find my.class.path.entity.resource.site.Site with id 738; nested exception is javax.persistence.EntityNotFoundException: Unable to find my.class.path.entity.resource.site.Site with id 738
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:373)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:507)
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:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
...
Stacktrace (5)
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-06-06 15:31:54,840 ERROR SpringApplication - Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:793)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1234)
at my.class.path.OfficerSubscription.main(OfficerSubscription.java:44)
Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Multiple representations of the same entity [my.class.path.entity.resource.site.Site#738] are being merged. Detached: [FJE84 - Uckfield]; Detached: [FJE84 - Uckfield]; nested exception is java.lang.IllegalStateException: Multiple representations of the same entity [my.class.path.entity.resource.site.Site#738] are being merged. Detached: [FJE84 - Uckfield]; Detached: [FJE84 - Uckfield]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:365)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:507)
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:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy122.save(Unknown Source)
at my.class.path.OfficerSubscription.run(OfficerSubscription.java:81)
at my.class.path.OfficerSubscription$$FastClassBySpringCGLIB$$705870eb.invoke(<generated>)
...
In JPA we use the @ManyToMany annotation to model many-to-many relationships. This type of relationship can be unidirectional or bidirectional: In a unidirectional relationship only one entity in the relationship points the other. In a bidirectional relationship both entities point to each other.
Persist should be called only on new entities, while merge is meant to reattach detached entities. If you're using the assigned generator, using merge instead of persist can cause a redundant SQL statement.
After days of search, I finally solved this in my spring boot project.
Add follwing blocks in the application.yaml
file:
spring:
jpa:
properties:
hibernate:
enable_lazy_load_no_trans: true
event:
merge:
entity_copy_observer: allow
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With