And to follow up on my recent pledge to start blogging further about ORM, lets jump right into a recent topic. A thread on the CF-ORM mailing list brought up the topic of dealing with a bidirectional relationship.

It started with a simple observation. Given a bidirectional relationship between Parent and Child, where Parent has a one-to-many to Child, and Child has a many-to-one to Parent:

Parent = new Parent();
Parent.setParentName("Bob");
Child = new Child();
Child.setChildName("Daniel");
Child.setParent(Parent);
EntitySave(Parent);
		

The questioner asked why this correctly relates the Child to the Parent, but does NOT relate the Parent to the Child? The answer is simple, but might not be obvious.

In Hibernate, when you have a bidirectional relationship, you must tell the ORM engine which side of the relationship is the "owner". In other words, which side "controls" the relationship? You do this using the "inverse" attribute. So in the example above, one of the property declarations in Parent might look like this:

property name="child" fieldtype="one-to-many" cfc="Child" fkcolumn="parentId" cascade="all" inverse="true";
		

What this tells Hibernate is: this Parent component will have one or more Child components, but I want the Child components to be responsible for managing this relationship. It is in charge.

With this in mind, understanding why the earlier code block doesn't save the Child into the Parent should be easier. If I don't explicitly set both sides of the relationship, and if the Child doesn't do something to enforce this bidirectional relationship, it won't get set.

So, that leaves you with two basic options. You can set this up explicitly, like so:

parent = new Parent();
parent.setParentName("Bob");
child = new Child();
child.setChildName("Daniel");
child.setParent(Parent);
parent.addChild(child);
EntitySave(Parent);
		

Which works, but is kind of a drag. The common way this is done in Hibernate is to set up an association management method to enforce the relationship. So in the Child component, you would have a method like this:

public function setParent( parent )
{
	variables.parent = arguments.parent;
	if( !parent.hasChild( this ) )
	{
		parent.addChild( this );
	}
}
		

As you can see, when I set the Parent on the Child, this method also checks to see if that Child is associated with the Parent, and if it is not, it adds itself. With this in place, the earlier code block will work:

Parent = new Parent();
Parent.setParentName("Bob");
Child = new Child();
Child.setChildName("Daniel");
Child.setParent(Parent);
EntitySave(Parent);
		

Association management methods are a common way to help ease managing bidirectional relationships in Hibernate. Hopefully this helps make this more clear, in case anyone else runs into the same issue! If you like, you can read more about this in the Hibernate documentation.

Comments Comments (9) | del.ico.us del.icio.us | Digg It! Digg It! | Linking Blogs Linking Blogs | 1179 Views

Related Blog Entries

Comments

  • # Posted By Joe Rinehart | 12/16/09 1:30 PM

    Nice post, Brian! Essential read for anyone using CF9 ORM in any complex capacity.

    One thing missing in the setParent method is a check for nilling the parent. I've got this all templated in AS3 and Groovy, but not in CFML yet. setParent() could be expanded to:

    public function setParent( parent )
    {
    variables.parent = arguments.parent;
    if( isDefined("parent") && !parent.hasChild( this ) )
    {
    parent.addChild( this );
    }
    }

    Are you just leaving the correct addChild() implementation as a puzzle for readers? :)

  • # Posted By Henry Ho | 12/16/09 3:51 PM

    wouldn't it be easier if you define it like so:

    public function setParent( required Parent parent )

    ? required and typed, no need to test for null.

  • # Posted By Rick O | 12/16/09 6:55 PM

    I've been getting in habit of _not_ using inverse="" but setting up the relationship from both directions, instead. Viz a blog example, where users can have many posts:

    User.cfc:
    property name="Posts" fieldtype="one-to-many" cfc="Post" fkcolumn="userId" cascade="all" lazy="true";

    Post.cfc:
    property name="User" fieldtype="many-to-one" cfc="User" cascade="all" fkcolumn="userId" required="true" notnull="true" lazy="true";

    Then, in your logic code:
    rick = entityLoadByPk("User", 1);
    post = entityNew("Post");
    post.setTitle("Test Post");
    post.setUser(rick);
    entitySave(post);
    writeDump(rick.getPosts());

    This _does_ show the new post, the inverse relationship, but without having to set it explicitly.

    Of course, I'm not an ORM guru ... so maybe there's some down side to doing it this way instead of the other way? However, it's worked pretty well for me so far.

  • # Posted By Rupesh Kumar | 12/17/09 5:56 AM

    Great post Brian. This is very important to understand for anyone starting to build application using ORM.
    However, there is one small thing which I would like to add (I am just nitpicking ;-))
    In the example here, child IS "in charge" and parent is correctly set on the child. However, the child did not get persisted because EntitySave was not called on it. And since child was not added to the parent, EntitySave on parent did not cascade to the child.

    As Joe said, one must check to ensure that parent is not null (using isDefined() or IsNull() ) and parent does not contain the child already. You might also want to add the logic to break the relationship as well. So if I call child.setParent(null) or child.setParent(newParent), it will remove the child from existing parent and then set its parent to null or newParent.

    @Rick, In case of bi-directional relationship, the relationship must be set from both the sides - irrespective of 'inverse' attribute. As Brian said, "Inverse" decides who is owning the relationship or who is incharge of the relationship. It is used to decide what SQL query will be used for establishing the relationship. If inverse is not set on either side, then there will be one extra sql executed for the same relationship - Once from child to parent and another from parent to child.
    Setting "inverse" on 'child' property of the parent will say that only child-parent relationship will be used for executing SQL for relation.

  • # Posted By Henry Ho | 12/17/09 6:02 AM

    "If inverse is not set on either side, then there will be one extra sql executed for the same relationship"

    really? like.. the exact sql executed twice? that's dumb...

    where did you learn this fact? thx

  • # Posted By Rupesh Kumar | 12/17/09 6:12 AM

    Quote from Hibernate reference - section 1.2.6 (http://docs.jboss.org/hibernate/stable/core/refere...)

    "For you, and for Java, a bi-directional link is simply a matter of setting the references on both sides correctly. Hibernate, however, does not have enough information to correctly arrange SQL INSERT and UPDATE statements (to avoid constraint violations). Making one side of the association inverse tells Hibernate to consider it a mirror of the other side. That is all that is necessary for Hibernate to resolve any issues that arise when transforming a directional navigation model to a SQL database schema. The rules are straightforward: all bi-directional associations need one side as inverse. In a one-to-many association it has to be the many-side, and in many-to-many association you can select either side. "

    Also take a look at section 6.3.2 (http://docs.jboss.org/hibernate/stable/core/refere...)

  • # Posted By Henry Ho | 12/17/09 3:17 PM

    "one-to-many association it has to be the many-side"

    So is "inverse=true" default to the many-side in CF? Or do we still need to add that ourselves?

  • # Posted By Rick O | 12/17/09 4:31 PM

    Rupesh said:
    "If inverse is not set on either side, then there will be one extra sql executed for the same relationship - Once from child to parent and another from parent to child."

    I think you may be wrong on this -- or maybe we're not talking about the same thing. Using the code excerpt I have above, with logging enabled, I get the following flow:

    post.setUser(rick);
    entitySave(post);

    These line produce this SQL:

    12/17 15:21:15 [jrpp-1] HIBERNATE DEBUG -
    insert
    into
    orm_test_post
    (Title, userId)
    values
    (?, ?)

    Then:

    writeDump(rick.getPosts());

    Produces SQL:

    select
    posts0_.userId as userId1_,
    posts0_.id as id1_,
    posts0_.id as id1_0_,
    posts0_.Title as Title1_0_,
    posts0_.userId as userId1_0_
    from
    orm_test_post posts0_
    where
    posts0_.userId=?

    Which, while verbose, is exactly what I would expect it to do.

    I'm just not seeing this double-SQL you're referring to, nor do I see the need to use the inverse="" attribute or override any setters if you define your relationships as I outlined above.

    But, again, I admit there may be some nuance that I'm missing here ...

  • # Posted By Jamie Krug | 12/26/09 1:01 PM

    Great stuff! Thanks, Brian.

    Since I was longing for some clarification on the questions brought up in these comments, I thought I'd point out to other readers that Brian has addressed these in his subsequent post (it's also linked as a related entry, above, but I didn't catch that at first):
    http://www.briankotek.com/blog/index.cfm/2009/12/2...