Photo from Chile

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

Barney Boisvert made a comment on my last post on this topic, suggesting that a nice way to reduce the amount of code one has to write, and to boost performance, would be to have one side in a bi-directional relationship simply delegate to the other side. This way you are only writing the code to do the work in one object, and it also reduces the number of hasX() calls.

I changed my code to try this technique out, and I quite like it. Because the code is slightly different I figured I might as well write about it here, to keep my posts on this topic up to date.

Taking the same Country and Language cfcs that we looked at last time:

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}

I'm going to add the "real" code to the Language.cfc, like in the last post, and I'll also show the "deferring" code in Country.cfc. The Country.cfc code will be much smaller, so let's just take a look at that first:

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    public Country function init() {
10        if (isNull(variables.Languages)) {
11            variables.Languages = [];
12        }
13        return this;
14    }        
15    
16    public void function addLanguage(required Language Language)
17        hint="set both sides of the bi-directional relationship" {
18        arguments.Language.addCountry(this);
19    }
20
21    public void function removeLanguage(required Language Language)
22        hint="set both sides of the bi-directional relationship" {
23        arguments.Language.removeCountry(this);
24    }
25
26}

So, when I call addLanguage() on a Country object all that's going to happen is that the addCountry() method on the Language object that has been passed in will be called. All of the processing that must be done to set both sides of the relationship will be contained in that addCountry() method. The same applies to calling removeLanguage() on a Country object. Note also that I still need to default the Languages property to an empty array or I risk errors when working with brand new objects. OK, let's move on and take a look at the new version of 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 Language function init() {
10        if (isNull(variables.Countries)) {
11            variables.Countries = [];
12        }
13        return this;
14    }
15
16    public void function addCountry(required Country Country)
17        hint="set both sides of the bi-directional relationship" {
18        if (not hasCountry(arguments.Country)) {
19            // set this side
20            arrayAppend(variables.Countries,arguments.Country);
21            // set the other side
22            arrayAppend(arguments.Country.getLanguages(),this);
23        }
24    }
25
26    public void function removeCountry(required Country Country)
27        hint="set both sides of the bi-directional relationship" {
28        if (hasCountry(arguments.Country)) {
29            // set this side
30            var index = arrayFind(variables.Countries,arguments.Country);
31            if (index gt 0) {
32                arrayDeleteAt(variables.Countries,index);
33            }
34            // set the other side
35            index = arrayFind(arguments.Country.getLanguages(),this);
36            if (index gt 0) {
37                arrayDeleteAt(arguments.Country.getLanguages(),index);
38            }
39        }
40    }
41}

This is pretty much the same as the code that we looked at in the last post, with the difference being that we're going to check to see whether the add or remove is required, using the hasCountry() method, and then, if required, we're going to manually do the add and remove on both sides. As Barney pointed out in his comment, this reduces the number of times that a hasX() method has to be called to one, and also isolates all of the code in one object, which could potentially make maintenance simpler.

Again, I just want to point out that this new code is based on an idea that Barney shared with me via a comment on my last post, so the credit for this cleverness belongs to him.

TweetBacks
Comments
Awesome :) Very useful post Bob - Thanks!
# Posted By John Whish | 6/15/10 10:18 AM
Very useful information! I find the management of relationships in CF9 ORM one of the harder things to get my head around, and appreciate any ideas to make it easier! But I'm wondering, can you do something similar to this with a one-to-many and/or many-to-one relationship? They are managed a bit differently than many-to-many.
# Posted By Mary Jo | 8/4/10 10:18 AM
I was looking for the same thing Mary. If Parent and Child was in a one to many relationship, I'm guessing it would be like this:
Parent.cfc
public void function addChild(required Child Child) {
arguments.Child.setParent(this);
}

public void function removeChild(required Child Child) {
arguments.Child.remove();
}

Child.cfc
public void function setParent(required Parent Parent) {
// set this side
   variables.Parent = arguments.Parent;
// set the other side
arrayAppend(arguments.Parent.getChildren(), this);
}

public void function remove() {
// set the other side
   local.oParent = this.getParent();
index = arrayFind(local.oParent.getChildren(), this);
if (index gt 0) {
arrayDeleteAt(local.oParent.getChildren(), index);
}
// set this side
   variables.Parent= javacast("null", "");
EntityDelete(this);
}
# Posted By Dave | 8/30/10 12:30 PM