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.

Creating A CFC Generator Object With Illudium

In my last post I showed the raw code that you would need to call the Illudium PU-36 Code Generator outside of it's Flex front-end. That code is just ripe for abstraction. Really all you want is an object with a method signature that takes maybe a com-path, datasource, and table name and does the work of generating the CFCs for you right? OK, now that we're all agreed. The CFC is attached in a zip (just click the "download" link down yonder).

And here is some sample code showing how easy it is to use now that it's packaged up.

<cfset generator = CreateObject('component','_testing.CFC.generator') />
<cfset generator.init('YOURCFADMINPASSWORD','_testing.CFC.generated') />
<cfset generator.GenerateTableCFCs(DataSource='YOURDATASOURCE', Table='SOMETABLE') />

There is one thing that you might want to change depending on your usage. The generator is set up to generate two sets of CFCs based off of two Illudium template sets. A base/volatile set and a custom/non-volatile set. It will always overwrite the base/volatile set. But, it will only write the custom/non-volatile set if it does not exist.

Disclaimer: You will most likely need to edit the CFC a little for pathing issues. And this will not work unless you have patched Illudium per my previous article.(Or until Brian rolls the patch in, which i think he's gonna).

Illudium - Outside Its Flex Front-End

So now you know why I needed a database abstraction system. You also know that I chose the Illudium PU-36 Code Generator because of it's templating system. Having the templating system gave me full control to generate exactly the kind of CFCs that I wanted. I'll go over some of the ways that I modified the stock templates in another post in this series.

The biggest issue I had with Illudium was that it very quickly became tedious to have to go into the Flex interface, re-enter all the information in the form and re-generate the CFCs every time I made a tweak to the templates. And let's be honest. It's going to be an iterative process to get the templates right.

So, I decided to see if I could call the CFCs that back the GUI outside of it. Then, I would be able to run a script and quickly re-generate the CFCs whenever I wanted to. If I remember right, I found what CFC and method were being called by watching the new monitoring tools in CF8. Then I started digging into the code that the Illudium remote facade calls. I mocked up the code that should work to get back a package of generated code for one table. But, it didn't work right away. I had to change a little bit of the code in one method of codeService.cfc to get it working. Oddly, the file pathing seems to change when something is being called by remoting vs. CreateObject().

Below is the updated code for that method. I've also attached a zip that has a patch file if you'd like to just patch the cfc in Eclipse.

<cffunction name="processCFMTemplate" access="private" output="false" returntype="string">
   <cfargument name="template" type="string" required="true" />
   <cfargument name="xmlTable" required="true" type="xml" />
   
   <cfset var content = "" />
   <cfset var root = arguments.xmlTable.root />
   <cfset var tempFileName = "#createUUID()#.cfm" />
   <cfset var tempDirPath = getDirectoryFromPath(getCurrentTemplatePath()) & "temp" />
   
   <cfif not directoryExists(tempDirPath)>
      <cfdirectory action="create" directory="#tempDirPath#">
   </cfif>
   
   <!--- write the cfm to a hard file so it can be dynamically evaluated --->
   <cffile action="write" file="#tempDirPath#/#tempFileName#" output="#arguments.template#" />
   <cfsavecontent variable="content">
      <cfinclude template="temp/#tempFileName#" />
   </cfsavecontent>
   <cfset content = replaceList(content,"<%,%>,%","<,>,##") />
   <cffile action="delete" file="#tempDirPath#/#tempFileName#" />
   
   <cfreturn content />
</cffunction>

Also, here is what the code for calling Illudium outside of the GUI looks like.

<!--- Path to Illudium xsl folder --->
<cfset xslBasePath = "/cfcgenerator/xsl/" />
<!--- Start of path to generated CFCs --->
<cfset ObjectStore = "com.generated.test" />

<cfset DataSource = "YOURDSNAME" />
<cfset Table = "ATABLENAME" />

<!--- Load Illudium PU-36 Code Generator Service --->
<cfset gs = createObject("component","cfcgenerator.com.cf.model.generatorService").init(xslBasePath) />
<cfset gs.setAdminPassword("YOURCFADMINPASSWORD") />

<cfscript>
   /* Illudium wants a path that doesn't include the trailing '/' */
   rootPath = mid(ExpandPath('/'),1,len(ExpandPath('/'))-1);
   
   /* Illudium Args */
   args = {
      dsn = DataSource,
      componentPath = ObjectStore & '.' & DataSource & '.' & Table,
      table = Table,
      projectPath = 'dealerpeak_base',
      stripLineBreaks = 'yes',
      rootPath = rootPath
   };
   basecode = gs.getGeneratedCFCs(argumentCollection=args);
</cfscript>

<!--- Separate folder for each Datasource --->
<cfset DataSourceDir = rootPath & '/' & replace(ObjectStore,'.','/','all') & '/' & DataSource />

<!--- Make sure datasource directory is there --->
<cfif NOT directoryExists(DataSourceDir)>
   <cfdirectory action="create" directory="#DataSourceDir#">
</cfif>
<!--- Create CFC files --->
<cfloop from="1" to="#arrayLen(basecode)#" index="i">
   <cffile action="write" file="#basecode[i].getFilePath()#" output="#basecode[i].getContent()#" />
</cfloop>

So, have fun playing with the Illudium PU-36 Code Generator! I'll be back soon to discuss bundling this up into a CFC generator object.

Impetus For A Database Abstraction System

I wrote a bit ago about using Brian Rinaldi's Illudium PU-36 Code Generator for a project at work. There was actually a pretty good response to the post. And people seemed interested in how I was using Illudium (outside of it's Flex front-end). So, this will be the first post in a series about building a database abstraction system.

So, what was the impetus for all of this? I work on a massive Fusebox 3 application. A large portion of it was written before CFMX. So, most of our database access is in qry_xxx.cfm files. This is OK. At least it's fairly organized. The first time this was really not OK was when we started experimenting with Flex and remoting. For remoting, you really need to use CFCs. But, we really didn't want to just cut and paste the same query into a CFC and have to maintain it in two places. And we really didn't have the time or man-power to rewrite all our data access. So, we basically gave up the idea.

Then our application got bigger and bigger. We hired more people. And there was more not-OK-ness. Like, I'm sick of writing the same boiler plate SQL for every new table I make. And it's actually sort of a pain for multiple developers to go see if somewhere in our massive application the query that they want is already written. So, for some time now, I have been really wanting to find a solution to standardize our database access.

I tried at one point using Doug Hughes' Reactor framework. But, there were some issues with getting it to pick up schema changes on a live server under load. Also, my boss wasn't really a fan of it's black-box-ness. Transfer is cool too. But, it has that same black-box-ness.

So, for requirements at work, what I really needed was:

  • Something to generate CFCs that would handle our database access.
  • For the CFCs to be easily found and read once generated.
  • For there to be a way for us to customize the functionality of the objects.

At some point I found the Illudium PU-36 Code Generator. It fit the bill because it uses a templating system. So, you can customize the code that it generates. In my next post I'll go over using Illudium outside of it's Flex front-end.

BlogCFC was created by Raymond Camden. This blog is running version 5.6.002.