My entry late last week on association management methods prompted a number of comments. I was going to add another comment but this became quite long so instead I'm adding another blog entry.

The point of my last post was to talk about bidirectional relationships, particularly a one-to-many/many-to-one between two entities. Hibernate (and, thus, the CF9 ORM) has the ability to specify an "inverse" attribute on your relationship. To better show why you usually want to do this, let's look at an example.

Say I have two simple CFCs, a User and a Role:

component displayname="user" persistent="true" accessors="true"
{
    property name="id" fieldtype="id" ormtype="integer" generator="identity";
    property name="name" ormtype="string";
    property name="roles" singularname="role" type="array" fieldtype="one-to-many" cfc="Role1" fkcolumn="userId" inverse="true" cascade="all";
}

component displayname="role" persistent="true" accessors="true"
{
    property name="id" fieldtype="id" ormtype="integer" generator="identity";
    property name="name" ormtype="string";
    property name="user" fieldtype="many-to-one" fkcolumn="userId" cfc="User1" cascade="all";
}
		

If I run this code:

user = new User1();
role = new Role1();
user.addRole( role );
role.setUser( user );
EntitySave( user );
		

The executed SQL looks like this:

12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG -
insert
into
User1
(name)
values
(?)
12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1
12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1

12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - insert into Role1 (name, userId) values (?, ?) 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 2 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 2

Now, if I take the same setup, but remove the "inverse" attribute on my one-to-many, I get this SQL:

12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG -
insert
into
User2
(name)
values
(?)
12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1
12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1

12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - insert into Role2 (name, userId) values (?, ?) 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding null to parameter: 1 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 2 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 2

12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - update Role2 set userId=? where id=?

12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 1 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 1 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 2 12/21 12:02:57 [jrpp-22] HIBERNATE DEBUG - binding '1' to parameter: 2

Do you see the additional update statement? This happens because I've removed the "inverse" attribute. When I use inverse=true, I am telling Hibernate to IGNORE this side of the relationship, and that the OTHER side "owns" this relationship. So in this example, I am telling Hibernate that it should only synchronize changes to the Role side (the owner) with the database, and to ignore changes to the collection of Roles that the User is holding. If I remove the inverse attribute, Hibernate no longer knows which side of this relationship to ignore. As a result, it must explicitly manage BOTH sides. That is the reason for the extra SQL update statement: Hibernate is updating the Role to reflect the change to the user.roles collection. As you can see, this update is completely unnecessary, but Hibernate has no way to know that.

You can see this in action again any time you manipulate this relationship. For example moving a role from one User to another:

var user = EntityLoad( 'User1', 1 )[1];
var user2 = EntityLoad( 'User1', 2 )[1];
role = user.getRoles()[1];
user.removeRole( role );
user2.addRole( role );
role.setUser( user2 );
EntitySave( user );
EntitySave( user2 );
		

The two sets of generated SQL:

User 1 saved (inverse=true)
12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG -
select
user1x0_.id as id1917_0_,
user1x0_.name as name1917_0_
from
User1 user1x0_
where
user1x0_.id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1917_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1917_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - select roles0_.userId as userId1_, roles0_.id as id1_, roles0_.id as id1914_0_, roles0_.name as name1914_0_, roles0_.userId as userId1914_0_ from Role1 roles0_ where roles0_.userId=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1914_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - select user1x0_.id as id1917_0_, user1x0_.name as name1917_0_ from User1 user1x0_ where user1x0_.id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1917_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1917_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - select roles0_.userId as userId1_, roles0_.id as id1_, roles0_.id as id1914_0_, roles0_.name as name1914_0_, roles0_.userId as userId1914_0_ from Role1 roles0_ where roles0_.userId=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - update Role1 set name=?, userId=? where id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding null to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding null to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 3

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 3

User 2 saved (no inverse)
12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG -
select
user2x0_.id as id1915_0_,
user2x0_.name as name1915_0_
from
User2 user2x0_
where
user2x0_.id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1915_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1915_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - select roles0_.userId as userId1_, roles0_.id as id1_, roles0_.id as id1910_0_, roles0_.name as name1910_0_, roles0_.userId as userId1910_0_ from Role2 roles0_ where roles0_.userId=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1910_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '1' as column: userId1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning '2' as column: id1_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - select user2x0_.id as id1915_0_, user2x0_.name as name1915_0_ from User2 user2x0_ where user2x0_.id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1915_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - returning null as column: name1915_0_

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - select roles0_.userId as userId1_, roles0_.id as id1_, roles0_.id as id1910_0_, roles0_.name as name1910_0_, roles0_.userId as userId1910_0_ from Role2 roles0_ where roles0_.userId=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - update Role2 set name=?, userId=? where id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding null to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding null to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 3

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 3

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - update Role2 set userId=null where userId=? and id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - update Role2 set userId=? where id=?

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '2' to parameter: 1

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 2

12/21 12:19:21 [jrpp-23] HIBERNATE DEBUG - binding '1' to parameter: 2

With the SQL side by side, you can see the unnecessary update statements. As you might imagine, in a large system these can add up to a lot of extra updates to the database. As a result, I would recommend specifying inverse on your bidirectional relationships wherever possible.

A few other items I wanted to touch on. First, Joe Rinehart and Rupesh mentioned adding a check for null values in any association management method you create. This is a good idea, since the value being passed in could be a null. As a result, a typical association management method might look like this:

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

Rick Osborne mentioned not using inverse and having it automatically manage both sides of the relationship. However, intuition points to this not working, and my testing verifies this. So, for example, if I leave out the inverse="true" and try this:

var user = EntityLoad( 'User1', 1 )[1];
role = new Role1();
role.setUser( user );
EntitySave( role );
ORMFlush();

var rolesBeforeClear = ArrayLen( user.getRoles() );
OrmClearSession();
user = EntityLoad( 'User#i#', 1 )[1];
var rolesAfterClear = ArrayLen( user.getRoles() );
		

The rolesBeforeClear says 0, but rolesAfterClear says 1. And this makes sense. If I don't set the relationship on both sides, the User's role collection is still empty. The only reason it shows up after I clear the Hibernate session and reload the User is because both sides of the relationship are using the same foreign key in Role. The same would be true if I removed a Role only on the Role side (nulling out setUser()). So not using inverse isn't a substitute for managing the relationship.

And lastly, I wanted to mention a potential issue that could bite you if you aren't aware of it. Under the hood, Hibernate requires you to override certain Java methods, equals() and hashCode(), in order to let Hibernate determine object equality. There are a number of ways this can be done, and the ColdFusion team went with the simplest option: basing the logic in equals() and hashCode() on the object's primary key. Sounds good right? Each object will have a unique primary key, so that sounds like a good way to tell if two objects are actually the same Hibernate entity or not, right?

Well, not always. Consider this:

var user = new User1();
var role1 = new Role1();
var role2 = new Role1();
user.addRole( role1 );
result1 = user.hasRole( role1 );
result2 = user.hasRole( role2 );
		

Intuitively, you would expect result1 to be true, and result2 to be false, since I only added role1 to the User. But in reality, BOTH of these return TRUE. And again, once you understand what is happening under the hood, this makes sense. Both the new roles have the same primary key right now (null). Since Hibernate is basing the equality check on whether the user has a given role on the primary key, and since both roles have the same key, it appears that both are present in the collection! So, be aware of this when you're creating multiple new objects and doing any identity-based checks on them. This behavior can also be influenced by your choice of primary key generator, so YMMV depending on your chosen RDBMS and your method for generating the key.

OK, this has turned into a long post, so I'll leave it there for now. If anyone has any questions or comments, please let me know. Thanks.

Comments Comments (2) | del.ico.us del.icio.us | Digg It! Digg It! | Linking Blogs Linking Blogs | 1088 Views

Related Blog Entries

Comments

  • # Posted By Rick O | 12/23/09 7:36 PM

    I think I see where we're getting our wires crossed. You guys are explicitly setting both relationships, while I'm only setting one side and letting Hibernate handle the other side.

    That is, you guys are doing both of these:
    user.addRole( role );
    role.setUser( user );

    I'm only doing one of those, not both. (Which ever makes more sense.)

    I haven't tried the session clearing you mentioned, and the example code I used is on my other computer, but after the holidays I'll come back and give it another round of testing.

    Thanks for investigating this ... it's something I've wondered about but haven't had the time to do any serious poking around.

  • # Posted By Raymond Camden | 12/30/09 11:11 AM

    @RickO: I tried setting the relationship on just one side and ran into issues when I tried to add/delete an entity from the relationship. Are you saying you got it to work ok?