Database Abstraction With Illudium and onMissingMethod()

This is the long overdue wrap-up post in my series about using Illudium outside it's Flex front end as a service to generate CFCs.

To recap: I explained why I was doing this and with these tools. Then I showed how you could call Illudium outside of it's Flex front-end. And I showed how we could bundle that generation code up into a CFC.

So, here's where it really gets fun. Now, I can generate CFCs to whatever specification that I need. But, I definitely do not want to be writing a bunch of CreateObject()s everywhere with hard coded paths to beans and Gateways. So, I decided that a dbService (service object to hide the implementation details of my database CFCs) would be a very good idea. I also decided that, like Reactor, I would do run-time generation to speed up developing code for new tables.

I also decided that I would create an API where methods have the name of the table in them (like getUserGateway(), getNewUser(), and getUserByID(userid). This is made possible by the onMissionMethod() method of CFCs in ColdFusion 8. The code for that is below. There is a lot going in it. So, take a look and we'll talk about it below when you're ready.

<cffunction name="onMissingMethod" access="public" returntype="component"
         hint="Secret sauce that makes (get[thing]ByID(thingID),getNew[thing](),get[thing]Gateway()) method calls work.">

   <cfargument name="missingMethodName" type="string" />
   <cfargument name="missingMethodArguments" type="struct" />
   
   <cfset var table = "" />
   <cfset var argsList = "" />
   <cfset var args = {} />
   
   <cfif left(missingMethodName,6) EQ 'getNew'>
   
      <!--- New empty record --->
      <cfset table = mid(missingMethodName,7,len(missingMethodName)) />
      <cfreturn getBean(table).init() />
      
   <cfelseif left(missingMethodName,3) EQ 'get' AND right(missingMethodName,4) EQ 'ByID'>
      
      <!--- Record object loaded from DB --->
      <cfset table = mid(missingMethodName,4,len(missingMethodName)-7) />
      <cfset argsList = StructKeyList(missingMethodArguments) />
      <cfif listLen(argsList) GT 1>
         <!--- Multiple Arguments: just pass them through --->
         <cfset args = missingMethodArguments />
      <cfelse>
         <cfif argsList EQ "1">
            <!--- Single Positional Argument: Try to create PK by convention --->
            <cfset args[getPKForTable(table)] = missingMethodArguments[argsList] />
         <cfelse>
            <!--- Single Non-Positional Argument: Pass it through --->
            <cfset args[argsList] = missingMethodArguments[argsList] />
         </cfif>
      </cfif>
      <cfreturn getBean(table).init(argumentCollection=args).load() />
      
   <cfelseif left(missingMethodName,3) EQ 'get' AND right(missingMethodName,7) EQ 'Gateway'>
      
      <!--- Table Gateway Object --->
      <cfset table = mid(missingMethodName,4,len(missingMethodName)-10) />
      <cfreturn getGateway(table) />
      
   <cfelse>
      
      <cfthrow type="dbService.UnhandledMissingMethod"
             message="Method (#missingMethodName#) was called on com.dealerpeak.dbService and I don't know how to handle it." />

      
   </cfif>
</cffunction>

Hope I didn't loose you. This method is using some simple string functions to determine what type of object the caller wants back and for what table. Then it makes calls to utility methods that do the work. For example, say I have a handle on an instance of the dbService in a variable called 'dbService'. If I call this code "user = dbService.getNewUser()", then the onMissionMethod calls "<cfreturn getBean(table).init() />". And getBean() looks like this.

<cffunction name="getBean" access="public" output="false" returntype="component"
         hint="Given a table name, return an instance of a bean. (w/ runtime CFC generation).">

   <cfargument name="Table" required="true" type="string" />
   <cfset var beanPath = getCFCPath(arguments.Table) />
   <cfset var bean = {} />
   <cftry>
      <cfset bean = CreateObject('component',beanPath)._setDBService(this) />
      <cfcatch>
         <!--- Runtime Generation --->
         <cfset generateCFCs(arguments.Table) />
         <!--- Return bean --->
         <cfset bean = CreateObject('component',beanPath)._setDBService(this) />
      </cfcatch>
   </cftry>
   <cfreturn bean />
</cffunction>

I'm sure you can guess what the other utility methods for Gateways and DAOs look like based on that. You can also see how the runtime generation of CFCs occurs. Also, each object instantiated, gets a pointer back to the dbService.

Early on I decided that I wanted all of the SQL in the Gateways and DAOs. But, I wanted the beans to expose an Active Record like API for persistence operations. So each base bean (that will always be over-written when regenerated) has load(), save(), and delete() methods. Take a look below at how one of those works.

<!--- Convenience method so bean can save itself --->
<cffunction name="save" access="public" returntype="component" output="false"
         hint="Convenience method so bean can save itself. Calls out to dbService.">

   <cfargument name="LoadAfterSave" type="boolean" required="false" default="true" />
   <cfset variables.dbService.getDAO(variables.TableName).save(this,LoadAfterSave) />
   <cfreturn this />
</cffunction>

I won't go any farther into the details of how I customized the generated CFCs. I'll let you do your own exploration there. It has been a very iterative process. And I'm still tweaking my generator templates.

I hope you get some good ideas from this series. Please remember, this is only one way of doing it. Work politics forced me to do it this way. But, now, I'm glad that I got the chance. In the future when I'm using Transfer or Hibernate, I will appreciate how much work went into making them so flexible and powerful.

Comments
Gerald Guido's Gravatar Chris... that is AWESOME. I just read your previous post. I my mind is a whir thinking of the possibilities.
# Posted By Gerald Guido | 11/13/08 9:03 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.6.002.