Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to persist a new entity containing multiple identical instances of another unpersisted entity with spring-boot and JPA?

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>)
    ...
like image 284
Chris Avatar asked Jun 06 '18 12:06

Chris


People also ask

How do you persist many to many relationships in JPA?

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.

What is the difference between Merge and persist in JPA?

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.


1 Answers

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
like image 75
W.Man Avatar answered Oct 28 '22 01:10

W.Man