IllegalStateException with Hibernate 4 and ManyToOne cascading

I've got those two classes

MyItem Object:

@Entity
public class MyItem implements Serializable {

    @Id
    private Integer id;
    @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Component defaultComponent;
    @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Component masterComponent;

    //default constructor, getter, setter, equals and hashCode
}

Component Object:

@Entity
public class Component implements Serializable {

    @Id
    private String name;

    //again, default constructor, getter, setter, equals and hashCode
}

And I'm tring to persist those with the following code:

public class Test {

    public static void main(String[] args) {
        Component c1 = new Component();
        c1.setName("comp");
        Component c2 = new Component();
        c2.setName("comp");
        System.out.println(c1.equals(c2)); //TRUE

        MyItem item = new MyItem();
        item.setId(5);
        item.setDefaultComponent(c1);
        item.setMasterComponent(c2);

        ItemDAO itemDAO = new ItemDAO();
        itemDAO.merge(item);
    }
}

While this works fine with Hibernate 3.6, Hibernate 4.1.3 throws

Exception in thread "main" java.lang.IllegalStateException: An entity copy was already assigned to a different entity.
        at org.hibernate.event.internal.EventCache.put(EventCache.java:184)
        at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:285)
        at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
        at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:914)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:896)
        at org.hibernate.engine.spi.CascadingAction$6.cascade(CascadingAction.java:288)
        at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:380)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:323)
        at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:208)
        at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:165)
        at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:423)
        at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:213)
        at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:282)
        at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
        at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76)
        at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:904)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:888)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:892)
        at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:874)
        at sandbox.h4bug.Test$GenericDAO.merge(Test.java:79)
        at sandbox.h4bug.Test.main(Test.java:25)

Database backend is h2 (but the same happens with hsqldb or derby). What am I doing wrong?


Solution 1:

I had the same problem, and this is what I found:

The merge method traverses the graph of the object that you want to store, and for each object in this graph it loads it from the database, so it has a pair of (persistent entity, detached entity) for each object in the graph, where detached entity is the entity that is going to be stored, and persistent entity is gotten from the database. (In the method, as well as in the error message the persistent entity is known as 'copy'). Then these pairs are put in two maps, one with the persistent entity as key and the detached entity as value, and one with the detached entity as key and the persistent entity as value.

For each such pair of entites, it checks these maps, to see if the persistent entity maps to the same detached entity as before (if it has already been visited), and vice versa. This problem occurs when you get a pair of entities where doing a get with the persistent entity returns a value, but a get from the other map, with the detached entity returns null, which means that you have already linked the persistent entity with a detached entity with a different hashcode (basically the object identifier if you have not overridden the hashcode-method).

TL;DR, you have multiple objects with different object identifiers/hashcode, but with the same persistence identifier (thus referencing the same persistent entity). This is appearantly no longer allowed in newer versions of Hibernate4 (4.1.3.Final and upwards from what I could tell).

The error message is not very good imo, what it really should say is something like:

A persistent entity has already been assigned to a different detached entity

or

Multiple detached objects corresponding to the same persistent entity

Solution 2:

Same here, check your equals() method. Most probably is badly implemented.

Edit: I have verified that a merge operation will not work if you don't implement your Entity's equals() and hashCode() methods correctly.

You should follow these guidelines for implementing equals() and hashCode():

http://docs.jboss.org/hibernate/orm/4.1/manual/en-US/html/ch04.html#persistent-classes-equalshashcode

"It is recommended that you implement equals() and hashCode() using Business key equality. Business key equality means that the equals() method compares only the properties that form the business key. It is a key that would identify our instance in the real world (a natural candidate key)"

That means: You should NOT use your Id as part of your equals() implementation!

Solution 3:

Is your relationship between item and component unidirectional or bidirectional? If it's bidirectional make sure you don't have Cascade.MERGE calls going back up to Item.

Basically, the newer version of Hibernate has a entity map that contains a list of all things that need to be merged based on the call to merge() it will call merge and then move onto the next one, but keep things in the map, it will throw the error you state above "An entity copy was already assigned to a different entity" when it encounters an item that has already been dealt with. We found in our app when we located these "upward" merges in the object graph ie. on bidirectional links, it fixed the merge call.

Solution 4:

Had the same exception (hibernate 4.3.0.CR2) tiring to save a object which is having two copies of a child object, got fixed by, in the entity from :

@OneToOne(cascade = CascadeType.MERGE)
private User reporter;
@OneToOne(cascade = CascadeType.MERGE)
private User assignedto;

to just,

@OneToOne
private User reporter;
@OneToOne
private User assignedto;

i don't know the reason though