Menu

As simple as possible, as complex as necessary

The simplicity of jQuery style generic getters and setters in ColdFusion Part 3

25 May 2011

In this last post on jQuery style getters/setters, we'll add a couple of useful refinements to our Base.cfc (see the two previous posts for background).

Setting in bulk

Credit once again goes to Hal Helms for this first enhancement which is to allow several properties to be set for an object in one statement. So instead of setting each property individually...

myCat.name( "Tibbles" );
myCat.gender( "Male" );
myCat.personality( "timid" );

...we can pass a struct of the new values to be set in one go...

myCat.set
 ({
	name="Tibbles"
	,gender="Male"
	,personality="timid"
});

In Hal's version, he's apparently set up a separate "updateAttributes()" method to handle this, but I decided to just enhance my existing set()" method so it will accept either a name/value pair, or a struct of pairs to set. If you pass a struct, it will recursively call itself to set each individual pair. This ensures the same benefits and restrictions apply to the bulk setting as to individual calls without duplicating any code.

Here's the set() method in Base.cfc modified to support bulk setting.

void function set(  )
	hint="I'm a generic Setter: you can give me the property name and new value to set, or a struct containing multiple properties/values to set"
{
	if( IsNull( arguments[ 1 ] ) )
		Throw( message="Please pass either a name and value or a struct of names/values",type="Base" );
	local.arg1 = arguments[ 1 ];
	// I'll accept a struct of names/values
	if( IsStruct( local.arg1 ) ) 
	{
		for( var arg in local.arg1 )
		{
			set( arg,local.arg1[ arg ] ); // Recursive call: pass each name and value to myself for setting
		}
		return;
	}
	// Otherwise I'll expect 2 arguments: name and value
	if( NOT IsValid( "variableName",local.arg1 ) )
		Throw( message="Please pass a property name and a value",type="Base" );
	if( IsNull( arguments[ 2 ] ) )
		Throw( message="Please supply a value.",type="Base" );
	local.arg2 = arguments[ 2 ];
	try
	{
		// Get a reference to the CF generated generic setter for this property
		local.set = this[ "set" & local.arg1 ];
		// Execute the setter with the passed new value
		local.set( local.arg2 );
	}
	catch( any exception )
	{
		if( exception.message CONTAINS "is undefined" ) // genuine missing method
			Throw( message="The property ""#local.arg1#"" is not defined or not settable in component #getMetaData( this ).name#",type="Base" );
		rethrow;
	}
}

Although the above is CF9 specific, the set() method in the previously shown CF8 compatible Base.cfc could easily be adapted along the same lines.

Dynamic setting

An incidental benefit of the set() method is the ability to use it for dynamic setting, where you don't know at runtime which property needs to be changed. Again, all of the checks and constraints of the CF generic setters apply as if you'd used them directly.

myCat.set( form.propertyName,form.propertyValue );

Dynamic getting is possible in the same way with get().

Auto-converting empty strings to NULL in ORM

CF has had a reputation for not dealing terribly well with NULL values, either just pretending they don't exist or treating them as empty strings - although CF9 has brought improvements. SQL is very clear about the distinction however, and to save a NULL value as opposed to zero length string in the database, I've long used the following type of "trick" with the null attribute of cfqueryparam.

<cfquery>
UPDATE
	cats
SET
	personality	=	<cfqueryparam cfsqltype="cf_sql_var" value="#form.personality#" null="#( Len( form.personality ) LT 1 )#">
WHERE
	name	=	<cfqueryparam cfsqltype="cf_sql_var" value="#form.name#">
</cfquery>

If the form field for personality is blank, this will record a NULL in the database rather than an empty string.

In ORM, however, we are delegating SQL operations to Hibernate and if CF is populating our Entities from blank form fields we will end up with empty strings rather than NULLs in our database.

To circumvent this, we can add a couple of lines to our generic set() method which will convert any zero length strings to Java NULLs which Hibernate will then correctly persist as NULLs in the database, while CF will continue to output them as empty strings.

void function set(  )
	hint="I'm a generic Setter: you can give me the property name and new value to set, or a struct containing multiple properties/values to set"
{
	if( IsNull( arguments[ 1 ] ) )
		Throw( message="Please pass either a name and value or a struct of names/values",type="Base" );
	local.arg1 = arguments[ 1 ];
	// I'll accept a struct of names/values
	if( IsStruct( local.arg1 ) ) 
	{
		for( var arg in local.arg1 )
		{
			set( arg,local.arg1[ arg ] ); // Recursive call: pass each name and value to myself for setting
		}
		return;
	}
	// Otherwise I'll expect 2 arguments: name and value
	if( NOT IsValid( "variableName",local.arg1 ) )
		Throw( message="Please pass a property name and a value",type="Base" );
	if( IsNull( arguments[ 2 ] ) )
		Throw( message="Please supply a value.",type="Base" );
	local.arg2 = arguments[ 2 ];
	try
	{
		// Get a reference to the CF generated generic setter for this property
		local.set = this[ "set" & local.arg1 ];
		// Check if the value is an empty string
		local.valueIsNull = ( IsValid( "string",local.arg2 ) AND ( local.arg2	IS "" ) );
		// Execute the setter with the passed new value, defaulting to NULL if an empty string
		local.set( ( local.valueIsNull ? JavaCast( "Null","" ) : local.arg2 ) );
	}
	catch( any exception )
	{
		if( exception.message CONTAINS "is undefined" ) // genuine missing method
			Throw( message="The property ""#local.arg1#"" is not defined or not settable in component #getMetaData( this ).name#",type="Base" );
		rethrow;
	}
}

So with this version of set() in Base.cfc, let's add an empty string as the personality value and see if it's set to a NULL...

myCat = New Cat();
myCat.personality( "" );
WriteDump( IsNull( myCat.personality() ) );

And the answer is: YES

So imagine you were populating an ORM Entity from a form with some fields left blank, with this in place those fields would still be saved to the database as NULLs rather than empty strings.

Conclusion

Although it involves extra code, I hope I've shown in this mini-series not only the simplicity of the jQuery style in terms of readability and code reduction, but also the benefit of having Base getter/setter functionality that can be put to use to solve incidental generic issues.

As always, corrections and suggestions are very welcome.

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