Performance Zone is brought to you in partnership with:

John regularly blogs about ColdFusion, JavaScript and other web technologies and contributes to several FOSS projects. His hobbies include writing in the third person. John is a DZone MVB and is not an employee of DZone and has posted 32 posts at DZone. You can read more from them at their website. View Full User Profile

Using thin wrappers around arrays of structs

02.08.2013
| 2087 views |
  • submit to reddit

I noticed that Sean Corfield had posted on the FW/1 mailing list about using a thin wrapper around arrays of structs. This is something that I've experimented with before and I find it to be a useful technique. Although Sean has taken it to a much higher level than I have, I thought it might be worth sharing an example of what I'm doing.

I use ORM a lot when building applications, but there are times when it can result in an huge amount of SQL statements being run which can affect performance. For example, I've built a site which has Products, those products have Product Options which can have their own prices (imagine a Camera that comes in Black and Solid Gold - they will have different prices).

Originally I used ORM to retrieve a list of all the Products and then called the relationships to find out if they had multiple product options, price variations etc. Whilst this worked nicely, it was far from performant. I ended up replacing the use of ORM for this part (just for this part - not the whole application!!) and replaced it with a single SQL statement.

This is pretty much the SQL I used:

select product_id as id
  , product_title as title
  , product_imagethumb as thumbnail
  , count(option_id) variations
  , sum(option_instock) instock
  , min(option_cost) as mincost
  , max(option_cost) as maxcost
from products
  inner join options on option_product_id = product_id
group by product_id
  , product_title
  , product_imagethumb
  order by product_title

It was a bit more complex, but you get the idea

Of course with a query object you can't do any computed values (in Sean's post he uses the example of getAge(), where you want to work out the age based on the data stored in the database). I wanted to be able to have nice methods like hasPriceRange() or hasVariations(), without putting any logic in my views.

At this point I remembered Peter Bell's Iterating Business Object http://ibo.riaforge.org/ and decided to steal the concepts. Which resulted in something like:

ProductIterator.cfc

component {

  ProductIterator function init( q ){
    variables.recordset = [];
    variables.recordcount = 0;
    variables.cursor = 0;
    if ( StructKeyExists( arguments, "q" ) ){
      setQuery( arguments.q );
    }
    return this;
  }
  
  /* Methods for computed values
  ------------------------------------------------------------------------*/
  
  boolean function hasPriceRange(){
    return get( "mincost" ) != get( "maxcost" );
  }
  
  boolean function hasVariations(){
    return get( "variations" ) > 0;
  }
  
  
  /* Iterator methods
  ------------------------------------------------------------------------*/
  
  void function setQuery( required query q ){
    variables.query = arguments.q;
    variables.recordset = [];
    
    for ( var row in variables.query ){
      arrayAppend( variables.recordset, row );
    }
    variables.recordcount = variables.query.recordCount;
  }

  // returns true if not at the end of the records
  boolean function next(){
    if ( variables.recordcount > variables.cursor ){
      variables.cursor++;
      return true;
    }else{
      return false;
    }
  }

  // returns the value for current cursor position 
  any function get( key ){
    return variables.recordset[ variables.cursor ][ key ];
  }
  
  // returns an array of structs, only here for debugging
  array function getMemento(){
    return variables.recordset;
  }
  
  /* Allow for get*key*() style calls without needing to create getters 
  ------------------------------------------------------------------------*/
  any function onMissingMethod( missingMethodName, missingMethodArguments ){
    var prefix = Left( arguments.missingMethodName, 3 );
    if ( "get" == prefix ){
      var key = Right(arguments.missingMethodName, len( arguments.missingMethodName ) - 3 );
      return get( key );      
    }
    else{
      throw "method '#arguments.missingMethodName#' not found";
    }
  }
  
}

What it does is to take a query and convert it into an array of structs. I can then loop over the array using the next() method to check if I've got to the end. By utilising onMissingMethod I can keep the syntax of calls such as getID(). For any computed values I simply add my own methods (in this example they are hasPriceRange() and hasVariations().

To use it in your views is simple, here's a complete example:

<cfquery name="products"> 
    select product_id as id 
       , product_title as title 
       , product_summary as summary 
       , product_imagethumb as thumbnail 
       , count(option_id) variations 
       , sum(option_instock) instock 
       , min(option_cost) as mincost 
       , max(option_cost) as maxcost 
    from products 
       inner join options on option_product_id = product_id 
    group by product_id 
       , product_title 
       , product_imagethumb 
       , product_favoured 
       , product_group_id 
    order by title 
</cfquery>

<cfset ProductIterator = new ProductIterator( products )>
<!--- start view --->
<cfoutput>
<cfloop condition="#ProductIterator.next()#">
  ID: #ProductIterator.getID()#<br>
  Title: #ProductIterator.getTitle()#<br>
  hasPriceRange: #ProductIterator.hasPriceRange()#<br>
  hasVariations: #ProductIterator.hasVariations()#<hr>
</cfloop>
</cfoutput>

It's not nearly as clever as what Sean is doing but it's a useful technique

Published at DZone with permission of John Whish, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)