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:
- let the Interceptor decide if the entity is transient
- let the EntityPersister decide if the entity is transient
- 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.