We are having a problem with Hibernate related to the saving of objects with references to detched objects.
I have a simplified and self-contained example, the source code is attached below.
I also have a (maybe very bad) idea for a patch to fix the issue.
Basically, the problem is where we have a Person object being saved to the DB and the person object is associated with a detached address object that was loaded in a previous session.
The symptom of the problem is that Hibernate throws a LazyInitException while trying to load the detached object (which it shouldn't try to do, since it's detached) during the persist operation.
Ordinarily, the above outlined scenario works fine, Hibernate detects that the Address is detached and since it is only trying to issue an insert, it just uses the ID of the address to generate the insert statement for the Person. The problem comes when a seperate piece of code loads the object that was detached into the session, but doesn't assign it to the object we are later trying to persist. (The two pieces of code are pretty much orthogonal, they just happen to refer to the same object for different reasons)
The example code is very simple, so maybe it would be a better idea to just look at the attached code for an understanding of the problem we are having.
The problem appears to be in the org.hibernate.engine.ForeignKeys.isNullifiable(String, Object) method, when it is trying to deal with lazy properties.
The problem seems to be with this code here:
Code:
private boolean isNullifiable(final String entityName, Object object)
throws HibernateException {
if (object==LazyPropertyInitializer.UNFETCHED_PROPERTY) return false; //this is kinda the best we can do...
if ( object instanceof HibernateProxy ) {
// if its an uninitialized proxy it can't be transient
LazyInitializer li = ( (HibernateProxy) object ).getHibernateLazyInitializer();
if ( li.getImplementation(session) == null ) {
return false;
// ie. we never have to null out a reference to
// an uninitialized proxy
}
else {
//unwrap it
object = li.getImplementation();
}
}
When you uncomment the line that reloads the Address in my example below, this code will throw a lazy exception when trying to unwrap the LazyInitializer implementation, because it thinks it has a Proxy that's attached to the current session, but it doesn't: it has the detached one from a previously closed session.
I think if you changed
Code:
if ( li.getImplementation(session) == null ) {
to
Code:
if ( li.getImplementation(session) == null || li.getImplementation(session) != object) {
that the isNullifiable() method would then be dealing properly with detached objects.
I'm not sure if this would break other things in Hibernate since I'm not sure about the contract isNullifiable() method needs to fill in the surrounding Hibernate code, but it does look like a pretty easy fix.
I've looked in the Hibernate JIRA for this issue but can't seem to find anything related. I'd like to raise the issue and attach a patch; please let me know if this is a known issue or maybe an already fixed issue.
Hibernate version: 3.2.5 GA Mapping documents:
Code:
@Entity
@Table(name = "eg_sbt_address")
public class Address {
private Long id;
private String content;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Basic
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
@Entity
@Table(name = "eg_sbt_person")
public class Person {
private Long id;
private Address address;
private String name;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@ManyToOne(fetch = FetchType.LAZY, cascade = {})
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Basic
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Code between sessionFactory.openSession() and session.close():
Code:
@Test
public void hibernateTwoSessionTest(){
DataSource ds = getDataSourceFromSomewhere();
Ejb3Configuration cfg = new Ejb3Configuration();
cfg.setDataSource(ds);
cfg.setProperty("hibernate.hbm2ddl.auto", "update");
cfg.addAnnotatedClass(Person.class);
cfg.addAnnotatedClass(Address.class);
HibernateEntityManagerFactory emf = (HibernateEntityManagerFactory)
cfg.buildEntityManagerFactory();
SessionFactory sf = emf.getSessionFactory();
Session s = sf.openSession();
Transaction tx = s.beginTransaction();
Address createAddr = new Address();
createAddr.setId(System.currentTimeMillis());
createAddr.setContent("21 Jump St");
s.persist(createAddr);
tx.commit();
s.close();
s = sf.openSession();
tx = s.beginTransaction();
Address loadedAddr = (Address) s.load(Address.class, createAddr.getId());
tx.commit();
s.close();
s = sf.openSession();
tx = s.beginTransaction();
// Uncomment this line and the test will fail inside the persist()
// operation with a LazyInitException
// Address reloadedAddr = (Address) s.get(Address.class, createAddr.getId());
Person person = new Person();
person.setId(System.currentTimeMillis());
person.setName("Johnny Depp");
person.setAddress(loadedAddr);
s.persist(person);
tx.commit();
s.close();
}
Full stack trace of any exception that occurs:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:86)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:140)
at org.hibernate.engine.ForeignKeys$Nullifier.isNullifiable(ForeignKeys.java:137)
at org.hibernate.engine.ForeignKeys$Nullifier.nullifyTransientReferences(ForeignKeys.java:92)
at org.hibernate.engine.ForeignKeys$Nullifier.nullifyTransientReferences(ForeignKeys.java:70)
at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:311)
at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:144)
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 gekko.services.query.QueryServiceTest.hibernateTwoSessionTest(QueryServiceTest.java:1404)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59)
at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)
at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79)
at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87)
at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77)
at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42)
at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88)
at gekko.services.test.util.NameAwareTestClassRunner.invokeTestMethod(NameAwareTestClassRunner.java:39)
at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
at com.intellij.rt.junit4.Junit4TestMethodAdapter.run(Junit4TestMethodAdapter.java:62)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:90)