Photo from Chile

Managing Bi-directional Relationships in ColdFusion ORM - Array-based Collections

It's important to know that when you have a bi-directional relationship you should set the relationship on both sides when setting one side. There have been a number of discussions about this on the cf-orm-dev google group, including this one in which Barney Boisvert provides a very good explanation of why this is important. Brian Kotek has also written two articles on the subject in the past. If you're not already familiar with this topic I suggest you check out those links.

The general recommendation for addressing this requirement is to override the methods in your objects that set one side of the relationship (e.g., setX(), addX() and removeX()) so that they'll set both sides, rather than just the side of the object that was invoked. While doing some testing of the new CF9 ORM adapter for Model-Glue along with the new scaffolding mechanism that we're developing I needed to address this issue for a many-to-many bi-directional relationship. I found that there were a few wrinkles that made the task not quite as straightforward as I has originally imagined, so I figured I should share what I came up with.

The particular many-to-many in question used an array to store a collection of objects on one side, and a structure to store the collection of objects on the other side. I found that each of these implementations introduced their own wrinkles, so I'm going to start with a post about dealing with array-based collections and then follow up with a second post about struct-based collections. Let's start by looking at the cfcs in question. For this example I'm using Countries and Languages to experiment with many-to-manys. A County can have many Languages spoken in it and a Language can have many Countries in which it's spoken. Here's what the cfcs look like:

view plain print about
1component persistent="true" hint="This is Country.cfc"
2{
3    property name="CountryId" fieldtype="id" generator="native";
4    property name="CountryCode" length="2" notnull="true";
5    property name="CountryName" notnull="true";
6    property name="Languages" fieldtype="many-to-many" cfc="Language"
7        type="array" singularname="Language" linktable="CountryLanguage";
8}
9
10component persistent="true" hint="This is Language.cfc"
11{
12    property name="LanguageId" fieldtype="id" generator="native";
13    property name="LanguageName" notnull="true";
14    property name="Countries" fieldtype="many-to-many" cfc="Country"
15    type="array" singularname="Country" linktable="CountryLanguage"
16    inverse="true";
17}

In order to ensure that both sides of the relationship are set whenever one side is explicitly set I need to override addLanguage() and removeLanguage() in Country.cfc and I need to override addCountry() and removeCountry() in Language.cfc. The code in each is virtually identical, as both sets of collections are implemented as arrays, so let's just look at Language.cfc:

view plain print about
1component persistent="true" hint="This is Language.cfc"
2{
3    property name="LanguageId" fieldtype="id" generator="native";
4    property name="LanguageName" notnull="true";
5    property name="Countries" fieldtype="many-to-many" cfc="Country"
6    type="array" singularname="Country" linktable="CountryLanguage"
7    inverse="true";
8
9    public void function addCountry(required Country Country)
10        hint="set both sides of the bi-directional relationship" {
11        // set this side
12        if (not hasCountry(arguments.Country)) {
13            arrayAppend(variables.Countries,arguments.Country);
14        }
15        // set the other side
16        if (not arguments.Country.hasLanguage(this)) {
17            arguments.Country.addLanguage(this);
18        }
19    }
20
21    public void function removeCountry(required Country Country)
22        hint="set both sides of the bi-directional relationship" {
23        // set this side
24        var index = arrayFind(variables.Countries,arguments.Country);
25        if (index gt 0) {
26            arrayDeleteAt(variables.Countries,index);
27        }
28        // set the other side
29        if (arguments.Country.hasLanguage(this)) {
30            arguments.Country.removeLanguage(this);
31        }
32    }
33}

Let's walk through the code and discuss some of the issues I had to address, starting with the addCountry() method. Because I'm overriding the implicit addCountry() method I have to implement it myself, which means that I have to add the Country object that was passed in to the current Language object. I first check to see if the Country is already present in the Countries collection using the hasCountry() method, and if it is not then I add it to the Countries collection using arrayAppend(). Next I have to set the other side, meaning I have to add the current Language object to the Country object that was passed in. This is a simple matter of calling addLanguage() on the Country object and passing in this, which is the current Language object. You'll notice that before I do that I first check to make sure that the current Language isn't already assigned to the Country. Do you know why that's necessary?

If I didn't do that check then this routine would call the addLanguage() routine in Country.cfc, which would turn around and call the addCountry() routine in Language.cfc, which would then call the addLanguage() routine in Country.cfc, ad infinitum, and we'd have a marvelous infinite loop. I personally don't like infinite loops creeping into my code, so I make sure that I only call the addLanguage() method if the Language has not already been assigned.

Let's move on the the removeCountry() method. Just as with the addCountry() method, because I'm overriding the implicit removeCountry() method I have to implement it myself, which means that I have to remove the Country object that was passed in from the Countries collection in the current Language object. Removing a specific item from an array is not as straightforward as adding an item to an array, so I have to use arrayFind() to first locate the Country in the array and then use arrayDeleteAt() to remove it. I can then set the other side exactly as I have done in the addCountry() method. I use hasLanguage() to see whether the current Language is assigned to the Country, and if it is then I use removeLanguage() to remove it.

This all works pretty well, but there is one situation in which errors can be thrown with the above code, and that's when we're working with a brand new object. When ColdFusion creates an instance of Language.cfc, for example when we call entityNew("Language"), all of the properties start off as nulls, including any collections. This means that if we create a new Language object and then try to add a Country object to it, we'll get an error on the line that reads:

arrayAppend(variables.Countries,arguments.Country)
because variables.Countries is not an array, it's a null. I've found the best way to deal with that is to default the collection to an empty array in the constructor. I add an init() method to my cfc that looks like this:
view plain print about
1public Language function init() {
2    if (isNull(variables.Countries)) {
3        variables.Countries = [];
4    }
5    return this;
6}

Now I can always count on variables.Countries being an array, and my code should work in all situations.

As I mentioned earlier, the approach that one must take when a collection is implemented as a struct, rather than an array, is a bit different and comes with its own set of wrinkles, so I plan to cover that in a future post.

Note also that an altogether different approach could be taken in which one creates new methods for managing the relationships. One might create assignCountry() and clearCountry() methods which, because they are not overriding the implicit methods, could simply make use of the implicit addCountry() and removeCountry() methods, which would eliminate much of the complexity required above. Another advantage of taking that approach is that one ends up programming to an interface rather than an implementation, which is always to be desired. The downside to that approach is that you are essentially changing the API of your object, and I wanted to avoid doing that in this specific case as I was trying to get code to work with a generic ORM adapter, which would have no idea that it should be calling assignCountry() rather than addCountry(). As with everything, design decisions are full of tradeoffs.

I am a little less than pleased with how complex this task seems to be and perhaps there are much better ways of tackling this than I have documented above. If anyone has any suggestions please leave them as comments.

TweetBacks
Comments
Just curious, but can't you set the default value of the countries property to an empty array when you define the property, rather than having to do it in the constructor?

And just a nitpick, is there a reason you use capitalized property names? Generally, only class names are capitalized. So I'd expect the property to be named "countries" and not "Countries".
# Posted By Brian Kotek | 3/29/10 12:57 PM
I did try to set the default value of the Countries property to an array using the default attribute, but it didn't work. Unfortunately you can only provide constants to the default attribute, not code. Not very flexible, eh?

Regarding the capitalized property names, that's just the way I've always done it. I realize that it's not what everyone does, but it's a habit that I've found difficult to break. ;-)
# Posted By Bob Silverberg | 3/29/10 1:08 PM
What I usually do is to set variables.Countries = [] outside of init() right after all properties are defined. Let the sudo-constructor handles that. Save myself a function (i.e. useless init), and an if statement.
# Posted By Henry Ho | 3/29/10 2:16 PM
Most of the time, I found the changes will need to be flushed then query again anyway, so in that case, I wouldn't bother changing the other side, just change the side with inverse=false, ormFlush() and query again.

Not a big fan of overriding those generated method too much.
# Posted By Henry Ho | 3/29/10 2:20 PM
Henry, to do this yourself is potentially bad. To tell others to do this is downright dangerous.

To anyone else, please don't do what Henry is advocating. Leaving the objects in an invalid state and "hoping" that everything is cleared and reloaded is a recipe for complete disaster. If you have bidirectional relationships, you MUST enforce the validity of those relationships. Doing this in the setters is the best option since it ensures that the relationship is properly maintained without breaking encapsulation.
# Posted By Brian Kotek | 3/29/10 2:35 PM
Oops, I'm not Telling others what to do. I'm just Sharing what I'm doing. Maybe I'm really doing it wrong then, thx Brian.
# Posted By Henry Ho | 3/29/10 2:38 PM
One thing you can do to reduce the duplication is to have country.addLanguage just invoke language.addCountry. Since the relationship is symmetric, you can save yourself the double implementation. In general, the CFC with the property declared "inverse" should delegate to the CFC with the non-"inverse" property to mirror the hibernate-level semantics of "inverse".

This also solves the cyclic problem: one side does both halves, the other side does nothing, so the delegation only happens in one direction.
# Posted By Barney Boisvert | 3/29/10 2:58 PM
That's a neat trick, Barney.

In that case you'd have to implement all of the manipulation manually, right? Meaning that you wouldn't call the add or remove methods on the other object from within your overridden method. Is that correct?
# Posted By Bob Silverberg | 3/29/10 3:33 PM
Yeah, you still have to do the manipulation manually as well as override the methods on both sides of the relationship. But it still ends up being less code than splitting it, because you don't have to check both sides twice for each manipulation.

Language.cfc:
function addCountry(cntry) {
cntry.addLanguage(this);
}

Country.cfc:
function addLanguage(lang) {
if (! hasLanguage(lang)) {
arrayAppend(languages, lang);
arrayAppend(lang.countries, this);
}
}

With the split implementation, calling country.addLanguage would result in four hasXXX checks, here there is only one.

The remove methods (which I've not shown) are equivalent, just with arrayFind/arrayDelete (or .remove()) instead of arrayAppend.
# Posted By Barney Boisvert | 3/29/10 3:45 PM
For anyone that may come in at this point and try to implement Barney's code in the previous comment, here's a change I had to make. In my case (translating to the above entities) I was getting an error that "COUNTRIES is undefined in LANG", because the underlying array is not publicly accessible.

To make it work, replace "arrayAppend(lang.countries, this);" with "arrayAppend(lang.getCountries(), this);".

At first glance it may seem like this won't work, because you're not modifying the underlying array, you're modifying a copy returned from the getter, right? Actually, no. CF passes arrays by reference, not by value, so when the array is returned and you operate on it, the changes will be persisted.
# Posted By Adam Tuttle | 7/27/11 9:54 AM
Adam,

Array's in CF are passed by value, not by reference and as per my testing I can't get yours/Barney's examples working.

See: http://www.bennadel.com/blog/275-Passing-Arrays-By... for details.

Dave
# Posted By David Ames | 10/2/11 11:45 PM