5

My Parent entity has a Child entity and collection of Elements:

@Entity
public class Parent {
    @Id
    private String id;

    @OneToOne(cascade = CascadeType.ALL)
    private Child child;

    @ManyToMany(cascade = CascadeType.ALL)
    private List<Element> elements;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Child getChild() {
        return child;
    }

    public void setChild(Child child) {
        this.child = child;
    }

    public List<Element> getElements() {
        return elements;
    }

    public void setElements(List<Element> elements) {
        this.elements = elements;
    }

}

The Child entity also has a collection of Elements, but it is restricted to contain only elements from the Parent's Elements collection. (but not necessarily all of them).

@Entity
public class Child {
    @Id
    private String id;

    @ManyToMany
    private List<Element> elements;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public List<Element> getElements() {
        return elements;
    }

    public void setElements(List<Element> elements) {
        this.elements = elements;
    }
}

Here is the Element entity:

@Entity
public class Element {

    public Element() {}

    public Element(String id) {
        this.id = id;
    }

    @Id
    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object other) {
        if (!(other instanceof Element)) {
            return false;
        }
        return id.equals(((Element) other).getId());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }
}

I want to pass an instance of Parent to save() method, so that the top level Element collection is created automatically (this is why I add cascade option to the first one). Parent's Child instance should also be saved, but it's elements should not be created, rather it should create reference to those that already exist in the top level collection (so no cascade in Child).

However, the following code produces exception (Spring's JPARepository internally calls EntityManager.merge())

    Parent parent = new Parent();
    parent.setId("someId");

    Element element1 = new Element("id1");
    Element element2 = new Element("id2");
    Element element3 = new Element("id3");

    Element element1Copy = new Element("id1");
    Element element3Copy = new Element("id3");

    List<Element> originalElements = new ArrayList<Element>();
    originalElements.add(element1);
    originalElements.add(element2);
    originalElements.add(element3);
    parent.setElements(originalElements);

    Child child = new Child();
    child.setId("childId");
    parent.setChild(child);
    List<Element> elementsCopies = new ArrayList<Element>();
    elementsCopies.add(element1Copy);
    elementsCopies.add(element3Copy);
    child.setElements(elementsCopies);


    Parent saved = parentRepository.save(parent);

Stack trace:

Caused by: org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find org.daniel.model.Element with id id1; nested exception is javax.persistence.EntityNotFoundException: Unable to find org.daniel.model.Element with id id1
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:389) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:491) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:147) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133) ~[spring-data-jpa-1.10.5.RELEASE.jar:na]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at com.sun.proxy.$Proxy73.save(Unknown Source) ~[na:na]
    at org.daniel.Monitor.createAndSave(Monitor.java:47) ~[classes/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_40]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_40]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_40]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_40]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:366) ~[spring-beans-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:311) ~[spring-beans-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:134) ~[spring-beans-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    ... 23 common frames omitted
Caused by: javax.persistence.EntityNotFoundException: Unable to find org.daniel.model.Element with id id1
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$JpaEntityNotFoundDelegate.handleEntityNotFound(EntityManagerFactoryBuilderImpl.java:144) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:227) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1129) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1022) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:639) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.EntityType.resolve(EntityType.java:431) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.EntityType.replace(EntityType.java:330) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:518) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.CollectionType.replace(CollectionType.java:663) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.AbstractType.replace(AbstractType.java:147) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.type.TypeHelper.replaceAssociations(TypeHelper.java:261) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:427) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:240) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:301) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:170) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:850) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:832) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.engine.spi.CascadingActions$6.cascade(CascadingActions.java:260) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:398) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:323) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:162) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:111) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:425) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:232) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:301) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:170) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:69) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:840) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:822) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:827) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:1161) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_40]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_40]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_40]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_40]
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at com.sun.proxy.$Proxy71.merge(Unknown Source) ~[na:na]
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:509) ~[spring-data-jpa-1.10.5.RELEASE.jar:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_40]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_40]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_40]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_40]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:503) ~[spring-data-commons-1.12.5.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:488) ~[spring-data-commons-1.12.5.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:460) ~[spring-data-commons-1.12.5.RELEASE.jar:na]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61) ~[spring-data-commons-1.12.5.RELEASE.jar:na]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136) ~[spring-tx-4.3.4.RELEASE.jar:4.3.4.RELEASE]
    ... 38 common frames omitted

The way I understand it is that Hibernate first processes the child (before the top level elements collection) and it fails to find Element instances that should be associated with the Child instance, because they are created when persisting the top-level elements collection.

How is this problem solved in Hibernate/JPA? Can I instruct it to persist my structure the way I want without falling back to manual field-by-field saving?

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
Danio
  • 1,064
  • 1
  • 11
  • 25

1 Answers1

5

Your current problem stems from the fact that you set the id of entities, which doesn't have a version attribute. Therefore Spring thinks this is an already persisted entity and tries to merge it with in the current EntityManager. But actually it is a new instance.

So in order to fix this you can do one of the following:

  • add an attribute to your entities and annotate it with @Version

  • let JPA generate your ID

I recommend the first approach, since a version attribute is usefull for other reasons as well.

But your problems won't stop there: You are creating multiple entities with the same type and id, this will cause Exceptions as soon as you have fixed the other problems.

Instead use just the same instance, like so:

    Parent parent = new Parent();
    parent.setId("someId");

    Element element1 = new Element("id1");
    Element element2 = new Element("id2");
    Element element3 = new Element("id3");

    List<Element> originalElements = new ArrayList<Element>();
    originalElements.add(element1);
    originalElements.add(element2);
    originalElements.add(element3);
    parent.setElements(originalElements);

    Child child = new Child();
    child.setId("childId");
    parent.setChild(child);
    List<Element> elementsCopies = new ArrayList<Element>();
    elementsCopies.add(element1);
    elementsCopies.add(element3);
    child.setElements(elementsCopies);


    Parent saved = parentRepository.save(parent);

Some more comments on your assumptions

I want to pass an instance of Parent to save() method, so that the top level Element collection is created automatically (this is why I add cascade option to the first one).

  1. You have to create the collection, JPA won't do that for you (but that seems just to be a problem of wording, because you do that just fine in your code)

  2. The cascade option has almost nothing to do with what records get created for entities, but only when. With CascadeType.ALL referenced entities will get saved when the entity at hand gets saved. Without cascading you would have to save it yourself. If you don't do that JPA will barf up an exception about a not persisted entity

Parent's Child instance should also be saved, but it's elements should not be created, rather it should create reference to those that already exist in the top level collection (so no cascade in Child).

  1. Again cascade has nothing to do with what database records get created only with who is responsible for it.

  2. If you want to things in JPA refer to the same 3rd thing, just use the same instance of the 3rd thing. JPA will figure it out.

The way I understand it is that Hibernate first processes the child (before the top level elements collection) and it fails to find Element instances that should be associated with the Child instance, because they are created when persisting the top-level elements collection.

As described at the top this is not at all the problem. Once you give JPA the instance graph that you want to be saved, it will figure out the correct order to do so (in almost all cases).

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
  • In real scenarios I will receive objects over REST and deserialize them, so I will have no control over instances creation - entities with the same ID will be different object instances. This is why I override equals (and hashCode) - because I thought this way I will force Hibernate to respect ID-based identity (rather than instance-based) – Danio Dec 27 '16 at 15:11
  • No, Hibernate will use always object identity internaly. Well if you have multiple detached instances with same id, Spring/Hibernate will merge them, resulting in a single instance, which will have the attribute values of the last instance that got merged. Do you have the properties of those Elements duplicated all over the place in your Rest request? Sounds strange to me. Anyway, if you want input how to properly handle that, that would be different question. – Jens Schauder Dec 27 '16 at 16:25
  • OK, so the conclusion is, I must deserialize same objects (in the sense of same id) to the same instances. – Danio Dec 29 '16 at 08:55