I am having trouble saving a new object with Hibernate which contains a nested collection that is mapped in hibernate as having `cascade="all, delete-orphan"`. More specifically, if I save the grandparent object it fails with the stack trace included below.
What's especially frustrating is we've come across this problem a few times, but it doesn't consistently appear in all collections we persist, only some of them and we don't know Hibernate well enough to see the common cause. Any insight here is greatly appreciated.
In general I seem to frequently have to debug into Hibernate's source when trying to track down what's going on with errors, this seems strange for such a mature library. It's also a very slow process since the Hibernate internals can be quite complex to someone who's just trying to use it.
An abbreviated version of my object classes, hibernate mapping files and a repro case are included below the stack trace.
I've also made a
StackOverflow Post about this problem.
Error w/ Stack Trace Code:
com.foo.server.services.db.exception.InvalidObjectException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.foo.server.model.house.conditioning.Duct
at com.foo.server.services.db.Transaction.commit(Transaction.java:33)
at com.foo.server.services.db.service.PersistenceManager.commitTransactionIfOpen(PersistenceManager.java:83)
at com.foo.server.services.db.service.DataService.save(DataService.java:230)
at com.foo.server.services.db.service.DataService.save(DataService.java:188)
at com.foo.server.model.house.TestHousePlan_JDO.testAddDistributionSystemSave(TestHousePlan_JDO.java:55)
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.foo.server.model.house.conditioning.Duct
at org.hibernate.engine.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:242)
at org.hibernate.collection.AbstractPersistentCollection.getOrphans(AbstractPersistentCollection.java:919)
at org.hibernate.collection.PersistentList.getOrphans(PersistentList.java:70)
at org.hibernate.engine.CollectionEntry.getOrphans(CollectionEntry.java:373)
at org.hibernate.engine.Cascade.deleteOrphans(Cascade.java:364)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:348)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:266)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:243)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:193)
at org.hibernate.engine.Cascade.cascade(Cascade.java:154)
at org.hibernate.event.def.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:154)
at org.hibernate.event.def.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:145)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:88)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:49)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1028)
at com.foo.server.services.db.Transaction.commit(Transaction.java:28)
... 31 more
HousePlan.java Code:
public class HousePlan
{
Long id;
List<DistributionSystem> distributionSystems = Lists.newArrayList();
// Other fields excluded
public HousePlan()
{
distributionSystems.add(new DistributionSystem());
}
// getters + setters
}
HousePlan.hbm.xml Code:
<hibernate-mapping package="com.foo.server.model.house">
<class name="HousePlan" table="house_plan">
<id name="id" column="id">
<generator class="native" />
</id>
<list name="distributionSystems" cascade="all,delete-orphan" lazy="false">
<key column="housePlanId" not-null="false" />
<list-index column="housePlanIndex" />
<one-to-many class="com.foo.server.model.house.conditioning.DistributionSystem" />
</list>
<!-- Other mappings excluded -->
</class>
</hibernate-mapping>
DistributionSystem.java Code:
public class DistributionSystem
{
Long id;
List<Duct> ducts = Lists.newArrayList();
// Other fields excluded
public DistributionSystem()
{
ducts.add(new Duct());
ducts.add(new Duct());
}
// getters + setters excluded
}
DistributionSystem.hbm.xml Code:
<hibernate-mapping package="com.foo.server.model.house.conditioning">
<class name="DistributionSystem" table="distribution_system">
<id name="id" column="id">
<generator class="native" />
</id>
<!-- Note the usage of `cascade="all,delete-orphan"` which is the normal solution to this problem -->
<list name="ducts" cascade="all,delete-orphan" lazy="false">
<key column="distributionSystemId" not-null="false" />
<list-index column="distributionSystemIndex" />
<one-to-many class="com.foo.server.model.house.conditioning.Duct" />
</list>
<!-- Other mappings excluded -->
</class>
</hibernate-mapping>
Duct.java Code:
public class Duct
{
Long id;
// Other fields excluded
// Constructor excluded
// getters + setters excluded
}
Duct.hbm.xml Code:
<hibernate-mapping package="com.foo.server.model.house.conditioning">
<class name="Duct" table="duct">
<id name="id" column="id">
<generator class="native" />
</id>
<!-- Other mappings excluded -->
</class>
</hibernate-mapping>
Repro case Includes abbreviated implementation of how we are using hibernate.
Code:
public class ReproCase
{
public static Session session = getSessionFactory().openSession();
public static void main(String... args)
{
HousePlan plan = new HousePlan();
// This save succeeds
plan = save(plan);
plan.getDistributionSystems().add(new DistributionSystem());
// This save fails
plan = save(plan);
}
public static <T> T save(T object)
{
Transaction transaction = new Transaction(session);
transaction.begin();
// In our full app our object inheritance hierarchy allows for the
// call to getId()
// Objects with null ids are transient. Objects with non-null ids are
// not transient, but might be detached from the persistence context so
// here we ensure the object is attached.
if (object.getId() != null)
{
object = (T)session.merge(object);
}
try
{
session.saveOrUpdate(object);
}
catch (Exception e)
{
session.merge(object);
}
// This is where the TransientObjectException occurs
session.flush();
transaction.commit();
return object;
}
}