Hibernate Books

All times are UTC - 5 hours [ DST ]



Post new topic Reply to topic  [ 3 posts ] 
Author Message
 Post subject: How to avoid "detached entity passed to persist" e
PostPosted: Fri Jan 23, 2009 12:26 pm 
Beginner
Beginner

Joined: Fri Jan 23, 2009 10:34 am
Posts: 25
Location: Switzerland
When doing an EntityManager.persist(myEntity), the entity is persisted only if its current state is "transient" (i.e. not yet persisted). We've got some problems while persisting objects for which Hibernate could not determine they are transient, so are sharing our experience here.

Software used:
  • Hibernate 3.3.1.GA
  • Webshpere 6.1.0.13 (JTA transactions)
  • Seam 2.0.0.GA

Lets define the following entity:

Code:
@Entity
@Table(...)
public class MyEntity {
  private long id;
  private String description;
  public MyEntity(long id, String description) {
    this.id=id;
    this.description=description;
  }

  @Id
  @Column(...)
  @NotNull
  public long getId() { return id; }
  public void setId(long id) { this.id=id; }

  @Column(...)
  public String getDescription() { return description; }
  public void setDescription(String description) { this.description=description; }
}


To persist, we would use the following code:

Code:
MyEntity myEntity = new MyEntity();
myEntity.setId(1);
myEntity.setDescription("fooBar");
entityManager.persist(myEntity);


Now, lets modify the entity identifier (key) mapping to make it generated by a custom IdentifierGenerator (an Hibernate interface implemented by my.package.MyIdentifierGenerator):

Code:
@Entity
@Table(...)
public class MyEntity {
  private long id;
  private String description;
  public MyEntity(long id, String description) {
    this.id=id;
    this.description=description;
  }

  @Id
  @Column(...)
  @NotNull
  @GeneratedValue(generator="myGenerator")
  @GenericGenerator(name="myGenerator", strategy="my.package.MyIdentifierGenerator")
  public long getId() { return id; }
  public void setId(long id) { this.id=id; }

  @Column(...)
  public String getDescription() { return description; }
  public void setDescription(String description) { this.description=description; }
}


The identifier generator is something like:
Code:
public class MyIdentifierGenerator implements IdentifierGenerator {
  public Serializable generate(SessionImplementor si, Object entity) {
    // WARNING: pseudo-Java code
    MyEntity myEntity = (MyEntity)entity;
    if (myEntity.getId()>0) {
      // the identifier has been set manually => use it
      return myEntity.getId();
    } else {
      // the identifier is not provided => generate it
      return getMyNextKey();
    }
  }
}


The code is now:
Code:
MyEntity myEntity = new MyEntity();
//myEntity.setId(1); // do not set the identifier as it is set by the my.package.MyIdentifierGenerator
myEntity.setDescription("fooBar");
entityManager.persist(myEntity);


However, if we want to force the key (identifier) to a specific value, i.e. to bypassing the custom IdentifierGenerator (which return the current object key or create a new one if the key does not exist), the code raises a PersistentObjectException:

Code:
MyEntity myEntity = new MyEntity();
myEntity.setId(1); // bypass the custom IdentifierGenerator and set an identifier not yet in the database
myEntity.setDescription("fooBar");
entityManager.persist(myEntity); // raise a PersistentObjectException


The exception raised is :

Code:
org.hibernate.PersistentObjectException: detached entity passed to persist: my.package.MyEntity
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
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)


Firstly, the problem is that the exception message should be more something like "cannot persist entity with identifier set manually AND via IdentifierGenerator". This would make debugging much easier.

Secondly, the exception should not occur because the object does not exist in the database with the given identifier.

The problem comes from the DefaultPersistEventListener.onPersist() method, which calls the parent AbstractSaveEventListener.getEntityState(), which return the entity state amongst the following: PERSISTENT, DELETED, TRANSIENT, DETACHED.
If not PERSISTENT, DELETED or TRANSIENT, the state is defaulted to DETACHED, which makes the exception to be raised. The two first states are checked using very simple tests and the TRANSIENT state is computed by the call to ForeignKeys.isTransient(), which tests the following operations:
  1. let the Interceptor decide if the entity is transient
  2. let the EntityPersister decide if the entity is transient
  3. ask the database to determine if the instance exists (i.e. not transient) or not (i.e. transient)
In case presented above, the "not TRANSIENT" decision is made by the EntityPersister.isTransient() method. For the simple test case here (no version, no cache, identifier provided), the EntityPersister determine the transient state by comparing the identifier with the default unsavedValue (the identifier value for a not-yet-persisted entity): if they are the same, the entity is transient. In our test case above, this is obviously not the case because the default identifier value is 0 and the identifier set is 1.

The code review above allowed to find the following workaround:

Code:
// define a new entity
MyEntity myEntity = new MyEntity();
myEntity.setId(1); // bypass the custom IdentifierGenerator and set an identifier not yet in the database
myEntity.setDescription("fooBar");

// backup the unsavedValue and replace it by any value (i.e. the entity will always be transient)
SessionImplementor session = (HibernateSessionProxy)entityManager.getDelegate();
EntityPersister persister = session.getEntityPersister(MyEntity.class.getName(), myEntity);
IdentifierProperty ip = persister.getEntityMetamodel().getIdentifierProperty();
IdentifierValue backupUnsavedValue = setUnsavedValue(ip, IdentifierValue.ANY);

entityManager.persist(myEntity);

// restore the backuped unsavedValue
setUnsavedValue(ip, backupUnsavedValue);

public IdentifierValue setUnsavedValue(IdentifierProperty ip, IdentifierValue newUnsavedValue) throw Throwable {
  IdentifierValue backup = ip.getUnsavedValue();
  Field f = ip.getClass().getDeclaredField("unsavedValue");
  f.setAccessible(true);
  f.set(ip, newUnsavedValue);
  return backup;
}


Now, the exception is no more raised and the entity is persisted into the database with the id=1.

Note: the whole process should probably be included in a critical section using a synchronized block on the IdentifierProperty "ip".

IMHO, the ForeignKeys.isTransient() method should be improved to consider the fact that EntityPersister.isTransient() did not look into the database to determine that the object is not transient:
Code:
Boolean isUnsaved = theEntityPersister.isTransient(...)
if (isUnsaved!=null) {
  if (isUnsaved.booleanValue()) {
    // the EntityPersister is 100% sure that the entity has not yet been
    // saved to the database and is thus transient
    // => accept it as TRANSIENT
    return Boolean.TRUE;
  } else {
    // the EntityPersister thinks that the entity has already been saved but
    // is not sure because it did not look into the database
    // => continue searching
    // do nothing (intentionally)
  }
}

.. continue the lookup process (i.e. assume, snapshot, see ForeignKeys.isTransient() for details)


While this concept is useful for a persist operation, it may imply a lot more database lookup if the isTransient() method is used for other operations (e.g. for update, I did not check). Note that the workaround has the advantage to avoid the database lookup.


Top
 Profile  
 
 Post subject: Re: How to avoid "detached entity passed to persist" e
PostPosted: Sat Mar 17, 2012 1:16 am 
Newbie

Joined: Fri Mar 16, 2012 6:55 am
Posts: 4
This what I would do instead:

// define a new entity
MyEntity myEntity = new MyEntity();
myEntity.setId(1); // bypass the custom IdentifierGenerator and set an identifier not yet in the database
myEntity.setDescription("fooBar");

/////////////////////////////////////////////////////////////////////////////////////////////////
// do not use persist, as you have done, instead use merge
// (can also use saveOrUpdate() or update() to generate the required "update" SQL)
entityManager.merge(myEntity);

Also see:

http://blog.xebia.com/2009/03/23/jpa-im ... -entities/


Top
 Profile  
 
 Post subject: Re: How to avoid "detached entity passed to persist" e
PostPosted: Tue Apr 03, 2012 8:37 pm 
Newbie

Joined: Tue Apr 03, 2012 8:21 pm
Posts: 1
I've been searching the forum for a while and so far I haven't found exactly what I need, but this is close, and actually has responses, so I'm hoping I can get some help.
I'm having issues with persist/merge.

I did something like this thread suggested, switching to merge so that I wouldn't have to worry about determining the state of my entities.
I'm using Hibernate 3.3.2 with EntityManager 3.4

My entity classes basically look like this:

Code:
@Entity
public class Event implements Serializable {

  @Id
  @Column
  @GeneratedValue
  private Integer id;

  ... More columns

  @OneToMany(mappedBy = "event", fetch = FetchType.LAZY)
//  @OrderBy(clause = "contact0_.firstName, contact0_.lastName") (commented out due to known bug)
  private Set<Attendance> attendances;
   //  ... accessors
}


Code:
@Entity
public class Contact implements Serializable {
  @Id
  @Column
  @GeneratedValue
  private Integer id;

  // .. More columns 

  @OneToMany(mappedBy = "contact", fetch = FetchType.LAZY)
  @JoinColumn(name = "contact_id")
//  @OrderBy(clause = "event1_.startTime desc") // Accounting for aliasing... I hope this works
  private Set<Attendance> attendances;
}


And the class that joins them (and has some other properties, which is why I'm not just using a many-to-many join
Code:
@Entity
public class Attendance implements Serializable {

  @Id
  @Column
  @GeneratedValue
  private Integer id;

  @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = Contact.class, optional = false)
  private Contact contact;

  @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = Event.class, optional = false)
  private Event event;

  // A couple more fields
}

And now for the problem. I have existing Event and Contact entities. I query to get an event, and copy it to a more portable "Message" class. I then manipulate this message class to add attendance records and send it back down to the server. The Event class has toMessage() and fromMessage() methods that handle this copying. So then I do this:

Code:
Event event = new Event();
entity.fromMessage(manager, message);
return manager.merge(entity).toMessage();


After the fromMessage() call, the event has been populated with all of the necessary values, including id. The Attendance classes in the collection will have their original ids if they existed, but in this case I was adding the first attendance records, so the collection has two new (transient) elements in it. I call merge(), and I have cascade = Cascade.ALL set, so my expectation is that new Attendance rows will be persisted as needed, and things will just work.

Instead I get
java.lang.IllegalStateException: org.hibernate.TransientObjectException: object is an unsaved transient instance - save the transient instance before merging: com.jnd.jnddb.model.Attendance

Is there a way to get hibernate to just Do the Right Thing?

Thanks in advance, I'm kind of at the end of my rope with this project.


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 3 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.