JPA cascade persist and references to detached entities throws PersistentObjectException. Why?
Asked Answered
H

2

33

I have an entity Foo that references an entity Bar:

@Entity
public class Foo {

    @OneToOne(cascade = {PERSIST, MERGE, REFRESH}, fetch = EAGER)
    public Bar getBar() {
        return bar;
    }
}

When I persist a new Foo, it can get a reference to either a new Bar or an existing Bar. When it gets an existing Bar, which happens to be detached, my JPA provider (Hibernate) throws the following exception:

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.Bar
 at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
 at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:636)
 at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:628)
 at org.hibernate.engine.EJB3CascadingAction$1.cascade(EJB3CascadingAction.java:28)
 at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:291)
 at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239)
 at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
 at org.hibernate.engine.Cascade.cascade(Cascade.java:153)
 at org.hibernate.event.def.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:454)
 at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:288)
 at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
 at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130)
 at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:49)
 at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:154)
 at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:110)
 at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:61)
 at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:645)
 at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:619)
 at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:623)
 at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:220)
 ... 112 more

When I either make sure the reference to Bar is managed (attached) or when I omit the cascade PERSIST in the relation, all works well.

Neither solution however is 100% satisfactory. If I remove the cascade persist, I obviously can't persist a Foo with a reference to a new Bar anymore. Making the reference to Bar managed necessitates code like this prior to persisting:

if (foo.getBar().getID() != null && !entityManager.contains(foo.getBar())) {
    foo.setBar(entityManager.merge(foo.getUBar()));
}
entityManager.persist(foo);

For a single Bar this might not seem like a big deal, but if I have to take all properties into account like this I'll end up with pretty horrible code that seems to defeat the reason of using ORM in the first place. I might as well well persist my object graph manually using JDBC again.

When given an existing Bar reference the only thing JPA has to do is take its ID and insert that in a column of the table that holds Foo. It does exactly this when Bar is attached, but throws the exception when Bar is detached.

My question is; why does it need Bar to be attached? Surely its ID won't change when the Bar instance transitions from detached to attached state, and that ID seems to be the only thing needed here.

Is this perhaps a bug in Hibernate or am I missing something?

Homs answered 28/11, 2010 at 0:27 Comment(3)
You could remove PERSIST from cascading, and check if (id == null) { em.persiste(foo.getBar()) } At least it makes your if simplier.Valdes
True, that would indeed make it slightly simpler. Thanks for the suggestion.Homs
Of course, even with slightly simpler if statements, the persisting code would still have to walk through the entire object graph, which is something I'm desperately trying to avoid.Homs
Y
21

You can use merge() instead of persist() in this case:

foo = entityManager.merge(foo); 

When applied to the new instance, merge() makes it persistent (actually - returns the persistent instance with the same state), and merges cascaded references, as you try to do manually.

Yuri answered 28/11, 2010 at 13:8 Comment(3)
Indeed, that seems to do the trick. merge() basically works as both the traditional persist and update operations it seems. I do think the javadoc could have been a little more clear on this issue though. I also still don't fully understand why persist() insists on the reference being managed even though there seems to be no reason for it, but practically merge() seems to be a pretty good alternative.Homs
Is it possible to do the "merge" not by the id, but by some other conditions? I get objects from an external source and if name and street birthday match, I want to retrieve that object. But the object is deeper in a cascade-persist- tree, so I won't do a load-from-database manually.Salpingotomy
What's the use of having a "merge" method AND a "persist" method then ? In this very case, we have to use a "merge" method to create an object, which is pretty weird. Or I'm I missing something ?Aforesaid
G
7

If I understand correctly, you just need the Bar reference to allow the new Foo to have the foreign key value (to the existing Bar) when persisting. There is a JPA method on the EntityManager called getReference() that might be useful to you for this case. The getReference() method is similar to find() except that it won't bother to return a managed instance (of Bar) unless it happens to already be cached in the persistence context. It will return a proxy object that will satisfy your foreign key needs in order to persist the Foo object. I'm not sure if this is the kind of solution you were hoping for, but give it a try and see if this works for you.

I also noticed from your code that you're using "property" style access instead of "field" style access by annotating your getter method (for the Bar relationship). Any reason for that? It's recommended that you annotate the members rather than the getters for performance reasons. It is supposed to be more efficient for the JPA provider to access the field directly rather than via getters and setters.

EDIT:

As someone else mentioned, using a cascade merge() will persist new entities as well as merge modified entities and reattach detached entites that have a relationship with the MERGE cascade option. Using PERSIST cascade option won't reattach anything or merge anything and is meant to be used when that is the behavior you want.

Grayce answered 28/11, 2010 at 2:43 Comment(3)
You are right. The Bar reference, IFF it is an existing entity, is only needed to allow the new Foo to have the FK to Bar. The getReference() looks indeed like a way cheaper alternative for find or merge() in this case. The problem however remains that I don't like to manually walk the object graph and replace all detached references with attached ones. I also still don't understand why JPA insist on having a managed reference or why there isn't a recursive "attach all" possible on a aggregation root.Homs
It is supposed to be more efficient for the JPA provider to access the field directly rather than via getters and setters. Hadn't heard about that before, but thanks for the tip!Homs
It isn't more efficient for all JPA implementations, perhaps just the one you're using. Others (e.g DataNucleus) provide the same efficiency with both waysSimpleton

© 2022 - 2024 — McMap. All rights reserved.