Photo from Chile

CF9 ORM Quick Tip - Removing All Items from A Collection

For this example of using ColdFusion 9's ORM features, let's assume we have a User object and a Department object. The Department has a collection of Users, like so:

view plain print about
1component persistent="true" output="false" entityname="Department" {
2    property name="DeptId" fieldtype="id" generator="native";
3    property name="Name";
4    property name="Users" fieldtype="one-to-many" type="array"
5        cfc="User" fkcolumn="DeptId" singularname="User";
6}

Let's also say that Department 1 currently has 3 Users assigned to it, call them User1, User2 and User3. Now, we want to write some code that will empty the Department's collection of Users. A first attempt might look something like:

view plain print about
1Users = Department.getUsers();
2for (i = 1; i LTE ArrayLen(Users); i = i + 1) {
3    Department.removeUser(Users[i]);
4}

But this won't work. In fact, if you run that code you'll see that you end up with a Department that still has User2 assigned to it. The reason this happens is that as soon as you remove User1 from the collection, the array shrinks, so User2 now occupies position 1 and User3 occupies position 2. So the next time through the loop, User3 gets removed (because i now equals 2) and Users[2] is User3. Then the loop finishes because the condition is met.

You might want to try doing an array loop using tags, like this:

view plain print about
1<cfset Users = Department.getUsers() />
2<cfloop array="#Users#" index="i">
3    <cfset Department.removeUser(i) />
4</cfloop>

Nope, this throws an error. CF sets the length of the array at the beginning of the loop, so by the time it's inside the third iteration it's trying to access Users[3], which no longer exists.

There are a couple of ways of achieving the objective. The simplest, is to call the setter for the collection and pass in an empty array:

view plain print about
1Department.setUsers([]);

Another is to forget about trying to loop over the array, and instead do a conditional loop based on the current size of the array:

view plain print about
1Users = Department.getUsers();
2while (ArrayLen(Users)) {
3    User.removeDept(Users[1]);
4}

Personally, I prefer the latter approach. The first seems a bit hacky to me, although both of them require that you program to the implementation of the collection (you are writing code expecting the collection to be an array). One way to make that a bit less painful would be to move whichever method you choose into the actual object itself. For example, in the Department object, add the following method:

view plain print about
1function void removeAllUsers() {
2    var Users = this.getUsers();
3    while (ArrayLen()) {
4        this.removeDept(Users[1]);
5    }
6}

Now you can call removeAllUsers() from outside of the object (e.g., from a Service) and nothing other than the object will have to know that your collection has been implemented as an array.

OK, so I snuck an OO design principle into my Quick Tip. It was hard to resist.

TweetBacks
Comments
Just to add to this, rather then using ArrayLen() in the conditional loop, I'd probably make use of the auto-generated hasUser() method. That should make the code a tad bit easier to read.
# Posted By Devin | 10/13/09 3:08 PM
Excellent idea. That's much better than using ArrayLen() - not only more readable, but also programming to the interface rather than the implementation of the collection.

Thanks!
# Posted By Bob Silverberg | 10/13/09 3:26 PM
I don't think passing in empty [] is hacky. Why does it seem hacky to you?
# Posted By Henry Ho | 10/13/09 3:33 PM
I guess it's the fact that rather than calling a "collection management" method, like addUser() or removeUser(), we seem to be simply stuffing data into a private variable. It's going to work in this example, but what if in a different situation you had additional code that gets executed whenever you remove a User from the collection? If you just call setUsers([]), then none of that additional logic will get executed.
# Posted By Bob Silverberg | 10/13/09 3:40 PM
calling setUsers([]) inside the removeAllUsers() function sounds good to me. I think it is more efficient.
# Posted By Henry Ho | 10/13/09 3:43 PM
@Bob, I had the same problem and solved it a slightly different way. My Dept object has this RemoveAllUsers method:

if ( this.hasUsers() )
{
var users = this.getUsers();
var i = 0;
for ( i=Arraylen( users ); i > 0; i-- )
{
this.removeUsers( users[ i ] );
}
}

@Henry, It may be more efficient but it is not really following the principle "Program to an interface, not an implementation.". As Bob said this could mean that any encapsulated logic in removeDept() is bypassed.
# Posted By John Whish | 10/14/09 6:21 AM
I think the "department.hasUser()" way is the cleanest but another way that worked for me is:

<cfset Users = Department.getUsers() />

<cfloop array="#Users#" index="User">
   <cfset OneUser = entityLoadByPK("User", #User.getID()#) />
   <cfset EntityDelete(OneUser) />
</cfloop>
# Posted By David | 10/15/09 11:32 AM
@David, What if you didn't want to delete the User, just the relationship between the department and the User?
# Posted By John Whish | 10/15/09 11:35 AM
@John, Sorry for the confusion, I had cascade="delete-orphan" so when I used the remove method it also deleted the record which was the same effect as using EntityDelete. But if cascade delete isn't turned on and you execute the remove method, then it simply sets the foreign key to null (aka removes the relationship).
Never mind me. :)
# Posted By David | 10/15/09 11:58 AM
Just wanted to say thanks. This helped me get through pretty much the same issue today. :)
# Posted By Adam Tuttle | 8/11/11 1:57 PM