Arf! Rails-Like ActiveRecord for ColdFusion

I'd like to introduce "Arf!" (Active Record Factory!), a Rails-style ActiveRecord implementation in ColdFusion. I have to disclaim this by saying that I have mixed feelings about ActiveRecord - it's at the same time very nice for doing things quickly, but I think it's ripe for abuse, as it's easy to think it's the begin and end all of OO programming. However, I think it's also a great way to sto...

[Read more on Joe Rinehart's blog]

Here's the basics of what the Arf! provides:

* JDBC metadata based reflection: not database specific

* Creates ActiveRecord API'd instances out of CFCs that extend a base ActiveRecord component

* Implements hasMany() and belongsTo() methods for establishing Record properties that point to other tables

* Allows for overloading any of the automagically generated methods to add custom business logic

* Automagic methods on Records include GetInstance(), Create(), Read(), Update(), Delete(), Save() [smart create/update], List(orderBy, whereClause), Validate() [does type and length checking], and SetNNN()/GetNNN() methods for each DB column

Code Demo: Arf! in Ten Steps

So, here's Arf! It's not ready for release yet, but I'd like to introduce it by showing how it'd be used to manipulate a two-table database in ten steps. If you like what you see, please let me know if you'd like to be on the alpha tester list.

Step 1: Create a database, tables, and datasource

I start by creating a database called "arfblog" (in any JDBC-compliant database, I've tested MySQL and MS SQL Server). I give it two tables: blogEntry (blogEntryId, title, body) and comment (commentId, blogEntryId, and message). The primary keys (blogEntryId and commentId) can be auto-incrementing numeric or VARCHAR(35) - if you go the VARCHAR route, Arf! will automagically use UUIDs for the key values. I then create a ColdFusion datasource pointing to the database and call it "arfblog"

Step 2: Configure an Arf! datasource bean

This'd be best done in ColdSpring or ChiliBeans (or your IoC container of choice), but you start by creating an Arf! datasource. It's just a little bean:

<cfset ds = createObject("component", "net.clearsoftware.arf.Datasource").init() />
<cfset ds.setDSN("arfblog") />
<cfset ds.setDatabase("arfblog") />

Step 3: Create an Arf! RecordFactory

Next, we create an instance of the Arf! RecordFactory itself. This is the CFC that performs the Arf! magic.

<cfset rf = createObject("component", "net.clearsoftware.arf.RecordFactory").init(ds, true) />

Step 4: Create an ActiveRecord extension

Now, we need to create a CFC that extends the base Arf! ActiveRecord CFC. It's pretty simple. I create a BlogEntry.cfc and Comment.cfc from the ActiveRecordTemplate.cfc that comes with Arf!. There's a single line of code that *must* be in the CFC, not in the base ActiveRecord:

<!--- BlogEntry.cfc --->
<cfcomponent extends="net.clearsoftware.arf.ActiveRecord" output="false">

<!--- Don't remove this line of code --->
<cffunction name="getFinalSuper" access="private"><cfreturn super /></cffunction>

<!--- Add statements like <cfset hasMany() /> and <cfset belongsTo() /> here --->

</cfcomponent>

Comment.cfc is identical to BlogEntry.cfc at this point

Step 5: Tell the classes about each other

A comment belongsTo an entry, and an entry hasMany comments, so we add the appropriate lines to each CFC, telling it what to relate to, and what CFC to use when getting related records.

<!--- BlogEntry.cfc --->
<cfcomponent extends="net.clearsoftware.arf.ActiveRecord" output="false">

<!--- Don't remove this line of code --->
<cffunction name="getFinalSuper" access="private"><cfreturn super /></cffunction>

<!--- Add statements like <cfset hasMany() /> and <cfset belongsTo() /> here --->
<cfset hasMany("comment", "net.clearsoftware.arf.test.Comment") />
</cfcomponent>

Now, we do the same for the Comment:

<!--- Comment.cfc --->
<cfcomponent extends="net.clearsoftware.arf.ActiveRecord" output="false">

<!--- Don't remove this line of code --->
<cffunction name="getFinalSuper" access="private"><cfreturn super /></cffunction>

<!--- Add statements like <cfset hasMany() /> and <cfset belongsTo() /> here --->
<cfset belongsTo("blogEntry", "net.clearsoftware.arf.test.BlogEntry") />
</cfcomponent>

Step 6: Create a new blogEntry

Ok, I can ask my factory for some classes now. Let's ask for a new blogEntry.

<cfset blogEntry = rf.makeRecord("net.clearsoftware.arf.test.BlogEntry") />

That gives us an ActiveRecord. It's got CRUD/S methods, get/set methods for each column in the blogEntry table, a getComment() method added by the hasMany() statement, a validate() method that'll check each property for NULL, length, and type, as well as a list() statement that has optional ORDER BY and WHERE arguments, and a getInstance() method that'll show us internal instance data (by value, where type makes it possible).

Step 7: Populate and save a blogEntry, showing that it's added with list()

<cfset blogEntry.setTitle("Some new entry") />
<cfset blogEntry.setBody("A really great blog entry!") />
<cfset blogEntry.save() />
<cfdump var="#blogEntry.list()#">

Not much to it, eh?

Step 8: Create a related comment

Now we'll create a related comment, and commit it.

<cfset comment = rf.makeRecord("net.clearsoftware.arf.test.Comment") />
<cfset comment.setBlogEntryId(blogEntry.getBlogEntryId()) />
<cfset comment.setMessage("I'm a wiseacre comment.") />
<cfset comment.save() />

Notice that we set its BlogEntryId property to the BlogEntryId value of the BlogEntry we made.

Step 9: Show our relation properties

Now, we'll see where those belongsTo() and hasMany() come into play. Remember saying that the comment belongsTo() a BlogEntry? Adding the following line of code asks the comment for its entry:

<cfset commentsEntry = comment.getBlogEntry() />
<cfdump var="#commentsEntry#" />
<cfdump var="#commentsEntry.getInstance()#" />

The first CFDump shows that getBlogEntry() returns a BlogEntry, and the second shows that its instance data is the data for the BlogEntry.

Now, hasMany() relationships can return more than one instance, so a simple QueryIterator is returned. It's got a pretty straightforward API, and you can get to its underlying query using QueryIterator.getQuery(). Here it is in action:

<cfset entryComments = blogEntry.getComment() />
<cfdump var="#entryComments.getQuery()#">
<cfset firstComment = entryComments.next() />
<cfdump var="#firstComment.getInstance()#" />

The first CFDump shows the query of related comments coming back, the second shows that that we can get a Comment instance from that query, and ask it for its instance data. We could also manipulate its data and save() it because its a fully-operational instance of our net.clearsoftware.arf.test.Comment CFC that extends ActiveRecord.

Step 10: Overriding methods

Because our "shell" CFCs (BlogEntry and Comment) extend ActiveRecord, we can alter the functionality of the default methods that are created for us. If we wanted to change the getTitle() method of BlogEntry to always come back in upper-case, we'd change our BlogEntry.cfc to the following:

<!--- BlogEntry.cfc --->
<cfcomponent extends="net.clearsoftware.arf.ActiveRecord" output="false">

<!--- Don't remove this line of code --->
<cffunction name="getFinalSuper" access="private"><cfreturn super /></cffunction>

<!--- Add statements like <cfset hasMany() /> and <cfset belongsTo() /> here --->
<cfset hasMany("comment", "net.clearsoftware.arf.test.Comment") />

<cffunction name="getTitle">
   <cfreturn uCase(super.getTitle()) />
</cffunction>

</cfcomponent>

Now, to show it works:

<cfoutput>
#blogEntry.getTitle()#
</cfoutput>

Ah, now that's good and annoying!

Conclusion

So, we've got a beginning of an ActiveRecord API / Generator working in ColdFusion. There's more to the API than what I've covered here. Hopefully, I'll get it documented and out the door before too long. If you'd like a copy to play with, please leave me a comment and I'll put you on the "alpha" tester list. I should have the code (it'll be LGPL, like Model-Glue) into the clearsoftware.net SVN repo before too long.

TweetBacks
Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
© 2018 Joe Rinehart
BlogCFC was created by Raymond Camden. This blog is running version 5.9.3.006.