Menu

As simple as possible, as complex as necessary

ColdFusion ORM quirks: removing one-to-many children

8 August 2014

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:

car cfdump showing 4 wheels

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" );

car cfdump showing wheels removed

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" );

car cfdump showing back wheels removed

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).

Posted on . Updated

Comments

  • Formatting comments: See this list of formatting tags you can use in your comments.
  • Want to paste code? Enclose within <pre><code> tags for syntax higlighting and better formatting and if possible use script. If your code includes "self-closing" tags, such as <cfargument>, you must add an explicit closing tag, otherwise it is likely to be mangled by the Disqus parser.
Back to the top