By Peter Bell

Snippets: The Base Iterating Business Object

This is an extremely lightweight implementation of an iterating business object. All the methods are in my base business object so they are available to all business objects.

Please see my earlier post for the benefits an iterating business object can provide built up from first principles . . .

The code is pretty simple. The object (I'm going to call it a bean as it will typically expose attributes through getters and/or setters) just needs to be able to load a recordset (I have some methods for loading from a struct and loading a column from a list, but I'm probably going to refactor those into a bean loader called by my bean factory to keep the base bean interface clean). It also needs the minimum iterator method set of first(), isLast() and next() (I'll probably also end up exposing the current record and the total recordcount, but I haven't needed to expose those yet and I like to keep my objects as shy as possible for as long as possible).

The purpose of the bean is to provide encapsulated access to a recordset containing 0..n records, exposing a set of attributes based on those properties using nicely encapsulated getters and/or setters. So, lets start with some code to load up the bean. Please note that the basis of this was a UDF written by Peter Farrell that I got off of CFLib. All it does is to load a query into an array of structures within the variables scope so I have easy programmatic access to the values within the bean by going to variables.Data[RowNumber][PropertyName] (e.g. the title for the 6th article would be variables.Data["6"].Title.

I'm not really sure what the load query should return. When I looked at my initial code I realized it was returning the bean and there was no real reason to do that. It could just have a returntype of void or a simple boolean for success, but for now I've changed it to return the recordcount successfully imported as that could be used for a little double checking or a simple "successfully loaded 12 records" kind of debugging message. I'll see how that does over time.

Note the package level access - this is only available to the same objects service object (or theoretically it's DAO) to keep everything nice and shy.

<cffunction name="loadQuery" returntype="any" access="package" output="false" hint="I take a query and use it to load the business object with its data.">
   <cfargument name="Recordset" type="query" required="yes" displayname="Recordset" hint="I am the query that needs to be loaded.">
   <cfscript>
      // Based on code by Peter J. Farrell (pjf@maestropublishing.com) via cflib.org       
       var theQuery = arguments.Recordset;
      var theStructure = StructNew();
      var cols = ListToArray(theQuery.columnlist);
      var row = 1;
      var thisRow = "";
      var col = 1;
      If (theQuery.recordcount LT 1)
      {   
         theStructure[row] = variables.setDefaultValues(theQuery.columnlist);
      }
      Else
      {
         for(row = 1; row LTE theQuery.recordcount; row = row + 1)
         {
            thisRow = StructNew();
            for(col = 1; col LTE arraylen(cols); col = col + 1)
            {
               thisRow[cols[col]] = theQuery[cols[col]][row];
            }
            theStructure[row] = Duplicate(thisRow);
            THIS.Recordset = arguments.Recordset;
         }
      };
      variables.Data = theStructure;
      variables.NumberofRecords = Row;
   </cfscript>
   <cfreturn This>
</cffunction>

What else do we need? Just the simple iterator methods of first(), next() and isLast() which manage the variables.IteratorRecord variable which keeps track of the current record. Please note, this is designed for request specific business objects for loading and displaying data. By its very nature it does not need to be thread safe, so the iterator doesn't need to worry about multiple requests playing with its recordcount - not part of the use case.

first()
The first() method just sets the iterator to 1. Should always be called before looping through the iterator just in case the object has been looped through elsewhere within the page (in theory this would never happen, but . . .).

<cffunction name="First" returntype="void" access="public" output="false" hint="I set the business object to point to the first record as part of the iterator functionality.">
   <cfscript>
      variables.IteratorRecord = 1;   
   </cfscript>
</cffunction>

next()
The next() method just increments the iterator so you can work with the next record. If you try to iterate past the last record, it just resets the iterator to point to the last record and returns "false" so you know you've gone too far.

<cffunction name="Next" returntype="boolean" access="public" output="false" hint="I increment the position of the business object iterator, returning success or failure as a boolean. If failure (already on final record), I leave the count the same.">
   <cfscript>
      // Declare a local structure for all local variables
      var Local = StructNew();
      If (variables.IteratorRecord GTE variables.NumberofRecords)
      {   
         Local.ReturnBoolean = false;
         variables.IteratorRecord = variables.NumberofRecords;
      }
      Else
      {
         Local.ReturnBoolean = false;
         variables.IteratorRecord = variables.IteratorRecord + 1;
      };
   </cfscript>
   <cfreturn Local.ReturnBoolean>
</cffunction>

isLast()
The isLast() method is used as part of the loop controller and returns a boolean. True if this is the last record, false if it isn't.

<cffunction name="isLast" returntype="boolean" access="public" output="false" hint="I return whether the business object is displaying the last record or not as part of the iterator functionality.">
   <cfscript>
      // Declare a local structure for all local variables
      var Local = StructNew();
      If (variables.IteratorRecord EQ variables.NumberofRecords)
         Local.ReturnBoolean = true;
      Else
         Local.ReturnBoolean = false;
   </cfscript>
   <cfreturn Local.ReturnBoolean>
</cffunction>

init()
Oh, and of course, for the above to work we just need a simple init() method as a pseudo constructor. It actually has a few additional properties, but I'm just showing the stripped down version. I'll show updated versions of the init() code as I show the extra methods in the base bean over the next few posts.

<cffunction name="init" returntype="BaseObject" access="public" output="false" hint="I initialize the base object.">
   <cfscript>
      variables.IteratorRecord = 1;
   </cfscript>
   <cfreturn This>
</cffunction>

You might be wondering what the display code would look like for (say) displaying a view or a list using such an object. It's actually very simple, but this has been a long post, so I'll post that in a separate article.

Comments
Ok...here's an idea (expanding upon our IM discussion). Why not have your first(), last() and next() items do all the work for you? Your loadQuery() method would simply set the query to a variables scope without any further transformation. Then add a private method that has queryRowToStruct (available at cflib). Then your first() method would basically set the iterator to 1 and so something like setMemento(queryRowToStruct(variables.query,variables.IteratorRecord) thereby setting instantiating your bean with the current rows values. Obviously next() would increment the iterator and do the same. This would mean that all the functionality you need is encapsulated in these few methods without the need for any methods to read your query converted to a structure. Does my explanation make sense?
# Posted By Brian Rinaldi | 7/27/06 9:03 AM
Hi Brian,

Makes plenty of sense. I think this is a better approach (which is why I blog about all of this stuff - so smarter people can show me the error of my ways!).

Many thanks. I'll redo this over the weekend and post the code Monday.

Best Wishes,
Peter
# Posted By Peter Bell | 7/27/06 9:06 AM
what about setters and sub-objects? These two factors might make doing a big, upfront query to array of structs convert more sensible.
# Posted By Ryan Miller | 7/31/06 12:50 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.