Hibernate One-to-Many Foray

We’re experiencing an issue where saving a GreetingSetPO does not result in the contained GreetingPO being saved with it — instead, we get an IllegalArgumentException deep in the guts of Hibernate.

The problem only happens when we use the JTA transaction manager along with envers (which makes use of beforeCompletion() Synchronization hooks).

I’ve posted to the envers forum already, and I’d like to post to the Hibernate forum, but I see that the Hibernate forum has a How To Ask For Help page.  Let’s go through that and make sure we’re doing the research they ask us to do, before posting.

Well, I see several interesting items in the Common Problems FAQ:

I saved a parent object but its associated objects weren’t saved to the database.

Associated objects must be saved explicitly by calling Session.save() (or Session.persist()) or the association must be mapped with cascade="all" or cascade="save-update" (or cascade="persist").

Now GreetingSetPO.getMembers() method is declared with cascade=all:

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "greetingSet")
    public Set<GreetingPO> getMembers() {
        return members;
    }

But GreetingPO.getGreetingSet() is not:

    @ManyToOne
    @JoinColumn
    public GreetingSetPO getGreetingSet() {
        return greetingSet;
    }

I wonder if that’s a problem?

I’m having trouble with a bidirectional association.

When you update a bidirectional association you must update both ends.

parent.getChildren().add(child);
child.setParent(parent);

It’s best to do this in an association management method of your persistent class.

Ok, we’re doing something like this… only we’re setting the whole set.  That is, instead of

        greetingSetPO.getMembers().add(greetingPO);

I’m doing

        Set<GreetingPO> greetings = new HashSet<GreetingPO>();
        greetings.add(greetingPO);
        greetingSetPO.setMembers(greetings);

I wonder if that would make a difference?

I’m still having trouble!

Read the documentation! There’s a detailed section about “Parent/Child Relationships” in chapter 16 of the reference documentation including code examples.

I need to read that chapter.

But first…

I’m getting ahead of myself though.  The Hibernate How To Ask For Help page has an ordering to it, and the first item is the road map for new users/Getting Started/I want to learn Hibernate for Java! page, and the first item there (after downloading Hibernate) is to do the tutorial.

The tutorial

So I’m reading through the tutorial in the Hibernate reference documentation. The examples are use the .hbm.xml configuration files rather than the class annotations we’re using — I’m trying to understand some concepts to apply later when I read about the annotations.

I did come to this, though: in section 1.3.6. Working bi-directional links

What about the inverse mapping attribute? For you, and for Java, a bi-directional link is simply a matter of setting the references on both sides correctly. Hibernate however doesn’t have enough information to correctly arrange SQL INSERT and UPDATE statements (to avoid constraint violations), and needs some help to handle bi-directional associations properly. Making one side of the association inverse tells Hibernate to basically ignore it, to consider it a mirror of the other side. That’s all that is necessary for Hibernate to work out all of the issues when transformation a directional navigation model to a SQL database schema. The rules you have to remember are straightforward: All bi-directional associations need one side as inverse. In a one-to-many association it has to be the many-side, in many-to-many association you can pick either side, there is no difference.

(Bold emphasis mine.)  And the Hibernate Annotations FAQ explains inverse this way:

How can I set inverse=”true”?

The semantic equivalent is mappedBy in the association annotations. Have a look at the reference guide for a complete explaination.

Now, our GreetingSetPO.getMembers()’ @OneToMany annotation does have the mappedBy attribute:


    @OneToMany(cascade = CascadeType.ALL, mappedBy = "greetingSet")
    public Set<GreetingPO> getMembers() {
        return members;
    }

I’m still not sure if it should be here or in GreetingPO…

I’m wanting to read that chapter about parent/child relationships the documentation pointed me to earlier.

Chapter 1621

It looks like the former chapter 16 is now chapter 21. Chapter 21. Example: Parent/Child starts out like this:

One of the very first things that new users try to do with Hibernate is to model a parent / child type relationship. There are two different approaches to this. For various reasons the most convenient approach, especially for new users, is to model both Parent and Child as entity classes with a <one-to-many> association from Parent to Child. (The alternative approach is to declare the Child as a <composite-element>.)

Gulp! That sounds like us!  Reading on…

Now, it turns out that default semantics of a one to many association (in Hibernate) are much less close to the usual semantics of a parent / child relationship than those of a composite element mapping. We will explain how to use a bidirectional one to many association with cascades to model a parent / child relationship efficiently and elegantly. It’s not at all difficult!

And a bit later:

[A]dding an entity to a collection does not cause that entity to become persistent, by default.

Instead, the default behaviour is that adding an entity to a collection merely creates a link between the two entities, while removing it removes the link. This is very appropriate for all sorts of cases. Where it is not appropriate at all is the case of a parent / child relationship, where the life of the child is bound to the life cycle of the parent.

(Emphasis mine.)  Sounds like I need to keep reading.  Section 21.2. Bidirectional one-to-many has this:

The underlying cause of this behaviour is that the link (the foreign key parent_id) from p to c is not considered part of the state of the Child object…

[…]

Now that the Child entity is managing the state of the link, we tell the collection not to update the link. We use the inverse attribute.

<set name="children" inverse="true">
    <key column="parent_id"/>
    <one-to-many class="Child"/>
</set>

The following code would be used to add a new Child

Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c);
session.save(c);
session.flush();

Saving the Child Yields Unsaved Transient Instance

Notice in that last code snippet, it’s the child PO that we pass to session.save().  (We’ve been passing the parent — the greeting set.)
Changing our test to session.save() the GreetingPO instead of the GreetingSetPO, now we get a different error:


org.springframework.dao.InvalidDataAccessApiUsageException: object references an unsaved transient instance - save the transient instance before flushing: com.ontsys.db.GreetingSetPO; nested exception is org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.ontsys.db.GreetingSetPO
    at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:651)
    at org.springframework.orm.hibernate3.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:143)
    at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:72)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:905)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:715)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:701)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:321)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:116)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:635)
    at com.ontsys.db.GreetingDAO$$EnhancerByCGLIB$$8fe200fc.create(<generated>)
    at com.ontsys.db.EnversWithCollectionsTest.testComplexCreate(EnversWithCollectionsTest.java:120)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:73)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:46)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:180)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:41)
    at org.junit.runners.ParentRunner$1.evaluate(ParentRunner.java:173)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:220)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:38)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.ontsys.db.GreetingSetPO
    at org.hibernate.engine.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:242)
    at org.hibernate.type.EntityType.getIdentifier(EntityType.java:430)
    at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:265)
    at org.hibernate.type.TypeFactory.findDirty(TypeFactory.java:619)
    at org.hibernate.persister.entity.AbstractEntityPersister.findDirty(AbstractEntityPersister.java:3151)
    at org.hibernate.event.def.DefaultFlushEntityEventListener.dirtyCheck(DefaultFlushEntityEventListener.java:501)
    at org.hibernate.event.def.DefaultFlushEntityEventListener.isUpdateNecessary(DefaultFlushEntityEventListener.java:227)
    at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:150)
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219)
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:49)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1027)
    at org.springframework.orm.hibernate3.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:135)
    ... 36 more

Looks Right

The Hibernate Annotations Reference Guide says this in Section 2.2.5.3.2.1. Bidirectional Collections:

Since many to one are (almost) always the owner side of a bidirectional relationship in the EJB3 spec, the one to many association is annotated by @OneToMany( mappedBy=... )

@Entity
public class Troop {
    @OneToMany(mappedBy="troop")
    public Set<Soldier> getSoldiers() {
    ...
}

@Entity
public class Soldier {
    @ManyToOne
    @JoinColumn(name="troop_fk")
    public Troop getTroop() {
    ...
}

Troop has a bidirectional one to many relationship with Soldier through the troop property.

@Cascade

I added a @Cascade annotation to GreetingSetPO.getMembers() as suggested by Section 2.4.7. Cascade, yielding this:


    @OneToMany(cascade = CascadeType.ALL, mappedBy = "greetingSet")
    @Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE, org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
    public Set<GreetingPO> getMembers() {
        return members;
    }

…but I still get the unsaved transient instance error.

In case it was the way I was setting the greeting set

I even changed my GreetingSetPO to instantiate its Set<GreetingPO> internally, and updated my test from

        Set<GreetingPO> greetings = new HashSet<GreetingPO>();
        greetings.add(greetingPO);
        greetingSetPO.setMembers(greetings);

to

        greetingSetPO.getMembers().add(greetingPO);

…but I still get the unsaved transient instance exception when I try to save the GreetingPO record and have it cascade-save the associated GreetingSetPO.

Hmm.

[Update 2/9/2009: This post to the Hibernate Users forum may describe the same issue or a similar one… ]

[Update 3/26/2009: I logged issue HHH-3782 to the Envers bug tracker, with a sample project showing the error.  Adam said:

What you are missing, is the cascade on GreetingPO.getGreetingSet: the annotation there should be for example @ManyToOne(cascade = CascadeType.ALL).

With the cascade turned on, the test passes with and without Envers.

If I add the @ManyToOne annotation I still get the error, so I still don’t know what the issue is.  I think this is as likely me not knowing what I’m doing with Hibernate as an issue in Envers. ]

Advertisements

,

  1. #1 by Michael Albrecht on February 11, 2009 - 8:59 am

    Hi danielmeyer!

    I think the problem is the “mappedBy” attribute.

    Try your code with the following declarations and tell me your result.
    I’m rather sure there will be no more problems.

    @Entity
    public class Troop {
    @OneToMany(cascade = CascadeType.ALL)
    public Set getSoldiers() {

    }

    @Entity
    public class Soldier {
    @ManyToOne
    @JoinColumn(name=”troop_fk”)
    public Troop getTroop() {

    }

    If it is so, I can not really explain but a colleague of mine has got the same solution for me :-)

    He tolds me that the mappedBy attribute is avoiding cascading save.

    Have much fun
    Michael

  2. #2 by ernest on March 25, 2009 - 11:14 pm

    Please if someone had solved this issue, please send the explanation me, too. I cannot get rid of this problem.

  3. #3 by danielmeyer on March 26, 2009 - 7:14 am

    Ernest, see if Adam Warski’s comment on the issue I logged helps.

  4. #4 by Alexander Zagniotov on July 11, 2011 - 9:46 pm

    Hi Daniel,

    Thank you for your comment. Adams solution has helped me to pin point the problem in my code, which eventually helped to reach resolution.

    Cheers

  5. #5 by Isuru on October 31, 2012 - 1:55 am

    use getHibernateTemplate().merge(anyobject);

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s