ColdFusion ORM quirks: removing one-to-many children
In the first post in this mini series on ORM quirks, I looked at one-to-many relations and how sorting could be tricky when using a link table.
NOTE This post refers to the Adobe ColdFusion 9.0.1 ORM implementation.
This time we'll stay with one-to-many (1-M) relationships and consider what you might expect to be a more straightforward operation: removing children from the parent.
Test object: a car
Let's take a simple example of 1-M: one car has many (normally four) wheels.
Car.cfc
component persistent="true" accessors="true" table="cars"{
property name="ID" fieldType="id" generator="assigned";
property name="wheels"
singularName="wheel"
fieldType="one-to-many"
cfc="Wheel"
fkColumn="carID";
public function init(){
return this;
}
}
Wheel.cfc
component persistent="true" accessors="true" table="wheels"{
property name="ID" fieldType="id" generator="assigned";
property name="location";
property name="car" fieldType="many-to-one" cfc="Car" fkColumn="carID";
public function init(){
return this;
}
}
Let's instantiate a car and add two front and two back wheels:
// Create a car
car = EntityNew( "Car" );
// Create an array of wheels and add them to the car
wheels = [];
for( i=1; i LTE 4; i++ ){
wheels[ i ] = EntityNew( "Wheel" );
wheels[ i ].setID( i );
car.addWheel( wheels[ i ] );
}
//set their location
wheels[ 1 ].setLocation( "front" );
wheels[ 2 ].setLocation( "front" );
wheels[ 3 ].setLocation( "back" );
wheels[ 4 ].setLocation( "back" );
WriteDump( var=car,label="car with wheels" );
Which we can see dumped as:
Removing all wheels
Now, what if I want to remove all four wheels from my car?
In the Methods section of the dump above, you can see that the ORM has helpfully added a "generated method" called REMOVEWHEEL
, but there's no REMOVEWHEELS
method.
In fact if I'd omitted the singularName="wheel"
attribute in my car-wheels definition, this method would indeed be generated as REMOVEWHEELS
, but either way it only operates on one wheel at a time, which needs to be passed as an argument.
Not a problem, we can add our own removeAllWheels()
method to Car.cfc which removes them one by one in a loop:
Car.cfc
public void function removeAllWheels(){
for( var wheel in this.getWheels() ){
this.removeWheel( wheel );
}
}
Calling this method however results in an exception with no diagnostic information whatsoever apart from the stack trace which begins:
java.util.ConcurrentModificationException
I'm no java programmer, but the problem seems to arise from the inability to change java "collections" while iterating over them.
A nice workaround - found in this Stack Overflow answer - is to simply clear the array:
Car.cfc
public void function removeAllWheels(){
if( !IsNull( this.getWheels() ) )
ArrayClear( this.getWheels() );
}
car.removeAllWheels();
WriteDump( var=car,label="car with NO wheels" );
Removing some but not all wheels
That works nicely to completely empty the array, but what if we only want to remove the back wheels?
In that case we unfortunately need to get more verbose. Although the java/hibernate collection can't be changed during iteration, if we transfer the child items we want to remove to a separate ColdFusion array, individual removal then seems to work.
Let's add a new method to Car.cfc which allows us to remove only the front or back wheels:
public void function removeWheelsInLocation( required string location ){
if( IsNull( this.getWheels() ) )
return;
var wheelsInLocation = [];
for( var wheel in this.getWheels() ){
if( wheel.getLocation() IS location )
ArrayAppend( wheelsInLocation,wheel );
}
for( var wheel in wheelsInLocation ){
this.removeWheel( wheel );
}
}
Here we loop over all the wheels picking out those in the specified location. Instead of removing them we just add them to a temporary array wheelsInLocation
.
We then loop over the temporary array and call the generated removeWheel()
to dismount each from the car.
car.removeWheelsInLocation( "back" );
WriteDump( var=car,label="car with no back wheels" );
Update 21 August 2014
CF ORM guru John Whish has suggested a clever solution to avoid the double loop to conditionally remove child entities: use an index based loop going backwards.
public void function removeWheelsInLocation( location ){
if( IsNull( this.getWheels() ) )
return;
var wheels = this.getWheels();
var totalWheels = ArrayLen( wheels );// Calculate the length just once
for( var i=totalWheels; i > 0; i-- ){
if( wheels[ i ].getLocation() IS location )
this.removeWheel( wheels[ i ] );
}
}
Nice one John! (whose book ColdFusion ORM I thoroughly recommend).
Comments