Getting Started with Reactor
Posted on Feb 17, 2006
As I have blogged earlier, I recently got the chance to begin revisiting using Reactor as part of my application "infrastructure". Doug Hughes has done a great job of building Reactor into a powerful tool to handle all the tedium of creating your component beans, DAOs and Gateways (and Transfer Objects). This is fantastic in and of itself, but, as I have discussed, you could easily create a code generator to do this yourself (not on the fly I suppose, but it still is a major time saver). I want to cover some of the more powerful features of Reactor (and some of the basics) from the perspective of a beginner (and a nOOb).What, to me, makes Reactor so compelling is that it can handle alot of things that your run-of-the-mill component objects cannot. As you may know, it handles the relationships through a very straightforward xml syntax (even with linking tables). Doing this, your object has access to the objects that it is related to without having to load in a whole slew of DAOs and Gateways and such (or having to put alot of work into customizing each object). Along this note, you can easily customize your Reactor objects by simply modifying the generated code (each component has two peices, one intended for customization). In addition, Reactor allows you to extend your Gateway "on the fly" by using its query object. Using this tool, you can rather easily build complicated queries without ever having to directly code anything into your gateway. Ok...enough talk...how about some examples.
So I am in the middle of trying to redo my Open-Source List in a manner that would make it easier to manage and a *little* more user-friendly/informative (and I could give it a less ridiculous looking URL). I thought this was a great opportunity to test Reactor on a fairly simple application. It consists of three tables, resources (these are the links with additional info on license and description), categories (um...these are...categories) and a linking table for the two (as in resources can have many categories). My reactor xml configuration document was very simple:
<reactor> <config> <dsn value="remotesynthesis" /> <type value="mysql" /> <mapping value="/CFOSListData" /> <mode value="development" /> </config> <P>
<objects> <object name="OpenSourceResource"> <hasMany name="OpenSourceCategory"> <link name="OpenSourceResourceCategories" /> </hasMany> </object> <object name="OpenSourceCategory"> <hasMany name="OpenSourceResource"> <link name="OpenSourceResourceCategories" /> </hasMany> </object> <object name="OpenSourceResourceCategories"> <hasOne name="OpenSourceResource"> <relate from="resourceID" to="resourceID" /> </hasOne> <hasOne name="OpenSourceCategory"> <relate from="categoryID" to="categoryID" /> </hasOne> </object> </objects> </reactor>
Obviously, the config portion sets the overall settings for Reactor. Also obviously, there are three objects as described above. My resources have many categories by virtue of a linking table (and vice versa for the categories). Simple, right? Now, my resources know about their categories and my categories know about their resources and they all live in peace, love and harmony.
In my application.cfc onApplicationStart() method, I have one line that initializes Reactor as follows:
<cfset application.reactor = CreateObject("Component","reactor.reactorFactory").init(expandPath("reactor.xml")) />
Now let's take a look at the form (keeping in mind this isn't a finished application yet).
<cfset resource = application.reactor.createRecord("OpenSourceResource") /> <cfset categoryGateway = application.reactor.createGateway("OpenSourceCategory") /> <P>
<!--- default errors and message ---> <cfset errors = structNew() /> <cfset message = "" /> <P>
<!--- load an existing resource ---> <cfif structKeyExists(url,"resourceID")> <cfset resource.setResourceID(url.resourceID) /> <cfset resource.load() /> <!--- form processing logic ---> <cfelseif structKeyExists(form,"fieldNames")> <cfset resource.init(argumentCollection=form) /> <cfif resource.validate().hasErrors()> <cfset errors = resource.validate().getErrors() /> <cfelse> <cfset resourceCategory = application.reactor.createRecord("OpenSourceResourceCategories") /> <cfset resourceCategory.setResourceID(form.resourceID) /> <cfparam name="form.categoryID" default="" /> <!--- delete any existing categories no longer on the list ---> <cfset arrCurrentCategories = resource.getOpenSourceResourceCategoriesArray() /> <cfloop from="1" to="#arrayLen(arrCurrentCategories)#" index="i"> <cfif NOT listFind(form.categoryID,arrCurrentCategories[i].getCategoryID())> <cfset arrCurrentCategories[i].delete() /> </cfif> </cfloop> <!--- if there are new categories, save them ---> <cfif len(form.newCategories)> <cfset category = application.reactor.createRecord("OpenSourceCategory") /> <cfset resourceCategory.setDateAdded(now()) /> <cfloop list="#form.newCategories#" index="newCat"> <cfset newID = createUUID() /> <cfset category.setCategoryID(newID) /> <cfset category.setCategory(trim(newCat)) /> <cfset category.save() /> <!--- automatically add these as categories to the current resource (although at this point the resource may not exist) ---> <cfset resourceCategory.setCategoryID(newID) /> <cfset resourceCategory.save() /> </cfloop> </cfif> <!--- save existing categories ---> <cfif len(form.categoryID)> <cfloop list="#form.categoryID#" index="existCat"> <cfset resourceCategory.setCategoryID(existCat) /> <!--- I am not sure this is necessary, but it gave it something other than the pk to update otherwise it errored this could be defaulted in the generated Transfer Object ---> <cfset resourceCategory.setDateAdded(now()) /> <cfset resourceCategory.save() /> </cfloop> </cfif> <cfset resource.save() /> <cfset message = "Your entry has been saved." /> </cfif> <!--- for a new record set some defaults. I could and should probably just default these by modifying the generated Transfer Object ---> <cfelse> <cfset resource.setResourceID(createUUID()) /> <cfset resource.setDateAdded(now()) /> </cfif> <cfset qryExistingCategories = resource.getOpenSourceCategoryQuery() /> <!--- modify the query object to return the results in alphabetical order ---> <cfset objQry = categoryGateway.createQuery() /> <cfset objQry.getOrder().setAsc('OpenSourceCategory','category')> <cfset qryAllCategories = categoryGateway.getByQuery(objQry)> <P>
<div id="admin"> <a href="edit.cfm">Add New Record</a> | <a href="index.cfm">View List</a> </div> <cfform action="#CGI.SCRIPT_NAME#" method="post" format="xml"> <cfif NOT structIsEmpty(errors)> <cfformitem type="html" style="color:red;"> <cfoutput> Your submission has the following errors: <ul> <cfloop collection="#errors#" item="error"> <li>#errors[error]#</li> </cfloop> </ul> </cfoutput> </cfformitem> <cfelseif len(message)> <cfformitem type="text" style="color:red;"><cfoutput>#message#</cfoutput></cfformitem> </cfif> <cfinput type="hidden" name="resourceID" value="#resource.getResourceID()#" /> <cfinput type="hidden" name="dateAdded" value="#resource.getDateAdded()#" /> <cfinput type="text" name="title" label="Title" value="#resource.getTitle()#" required="true" validate="noblanks" /> <cfinput type="text" name="href" label="Href" value="#resource.getHref()#" required="true" validate="noblanks" /> <cfinput type="text" name="license" label="License" value="#resource.getLicense()#" required="false" /> <cfselect name="categoryID" label="Categories" multiple="true" query="qryAllCategories" display="category" value="categoryID" selected="#valueList(qryExistingCategories.categoryID)#" /> <cfinput type="text" name="newCategories" label="New Categories" required="false" /> <cftextarea name="description" label="Description" value="#trim(resource.getDescription())#" required="true" style="width:330px;height:200px;" /> <cfinput type="submit" name="submitted" value="submit" /> </cfform>
This may not appear easy (and not combining it with something like Model-Glue can make it look chaotic, but that is my next step). Essentially, I begin the form processing by initializing my resource record with the form fields and then use Reactor's built in validation (which is based upon the table metadata). If everything is ok, I begin by handling the removing any categories that you may have removed from the entry (i.e. they are no longer selected in the select box). Notice how I utilize the fact that my resource object knows about its categories to do this very simply (IMHO).
Next, I add any new categories you may have added (yes, this form bears similarites to Ray's blog edit.cfm although this wasn't by design). This is easy, I just keep adding a new id to each one, and Reactor knows that it is a new record and not an existing one. Finally, I save any items on the existing categories list. I did run into an issue here that may be my inexperience with Reactor, but since my linking table initialy had only two columns both of which made up my primary key, this caused errors since an update would have no columns to update (the query read ...SET WHERE...). I could have done this a number of ways, including skipping over the items that the object already had saved, but that logic seemed more difficult than just adding a silly date column (though it isn't technically necessary). I suppose you could just wrap this in a cftry and ignore errors to keep the logic simple (I think of that now). Either way, this solution seemed acceptable to me.
Finally, I need to save my object. This is tough (save()).
Before the form, you will notice that I tweak the query for returning all the categories so that they come back in alphabetical order. I will go into this more in the display page.
The display page code is very simple and straightforward.
<cfset resourceGateway = application.reactor.createGateway("OpenSourceResource") /> <!--- build a joined query of resources and resource categories ---> <cfset objQry = resourceGateway.createQuery() /> <cfset objQry.join('OpenSourceResource','OpenSourceResourceCategories') /> <cfset objQry.join('OpenSourceResourceCategories','OpenSourceCategory') /> <cfset objQry.returnObjectFields("OpenSourceResource") /> <cfset objQry.returnObjectFields("OpenSourceCategory") /> <cfset objQry.getOrder().setAsc('OpenSourceResource','title')> <cfset objQry.getOrder().setAsc('OpenSourceResource','resourceID')> <cfif structKeyExists(url,"categoryID")> <cfset objQry.getWhere().isEqual('OpenSourceCategory','categoryID',url.categoryID) /> </cfif> <cfset qryResources = resourceGateway.getByQuery(objQry) /> <P>
<div id="admin"> <a href="edit.cfm">Add New Record</a> | <a href="index.cfm">View List</a> </div> <cfoutput query="qryResources" group="resourceID"> <div id="resource"> <p><span class="title"><a href="#qryResources.href#">#qryResources.title#</a></span> | <span class="edit"><a href="edit.cfm?resourceID=#qryResources.resourceID#">edit</a></span><br /> <span class="description">#qryResources.description#</span><br /> <cfif len(qryResources.license)> <span class="license">License: #qryResources.license#</span><br /> </cfif> <span class="categories">Categories: <cfoutput group="categoryID"> <a href="#CGI.SCRIPT_NAME#?categoryID=#qryResources.categoryID#">#qryResources.category#</a> </cfoutput> </span> <p> </div> </cfoutput>
Essentially, I build a query that joins the tables and then just do a basic display (I mean this really isn't a complex app). In reality, I could have used the gateway as is and then populated the object and used its built in knwledge of its categories to acheive this same effect. This seemed clunky to me though since it would require twice as many queries (or more) as I have records to display. I thought it better to use Reactor's query object and build myself a joined query (forgive me if there is a better way to acheive this, plus the documentation on the query object isn't totally complete yet, so I figured most of this out through alot of cfdumps).
To query using the query object, you use the getByQuery() method of your gateway. This requires a Reactor query object as its only argument, so first you will need to createQuery() and then customize it. The syntax is straightforward. You will see I add a couple of joins between tables using the join() method. I then tell Reactor to return to me only the fields of resources and categories (I don't care for the fields in the linking table). Next I tell it to order first by title and then by resourceID. Lastly, if a categoryID is supplied in the URL, I tell it to filter by only those records in the given category. The output of this is straightforward and probably needs no real explanation (I always forget about grouping outputs...it's a feature I rarely use, but seemed appropriate here).
Anyway, that's it. The important part to know is that you are looking at all the code I wrote. As in, I never wrote a single component for this (nor did I modify the generated ones...though as noted in the notes in the code...I may make a couple of minor changes to set some defaults). I think this is awesome, and I hope to delve into it further, but I am fairly certain Reactor will become a fixture in my development both independently and (potentially) at my (paying) job. I will post more on the topic as I learn more about it. Oh, and look for my updated open-source list sometime later this month.
Thanks for the code samples man, here and elsewhere. Good stuff.
What is the state of locking now? Like, for example, should:
cfset resourceGateway = application.reactor.createGateway("OpenSourceResource")
Or was this just a quickie, and normally you'd use a scope facade or something like that?
Sorry for the noobish questions, and again, thanks!
Posted By denny / Posted on 08/16/2006 at 3:31 AM
So, I will admit I am terrible about locking anything. I am not sure if it is as big a deal in more recent versions of CF. Nonetheless, I wouldn't lock application.reactor.createGateway() because it isn't actually modifying anything in application scope, just accessing it.
Posted By Brian Rinaldi / Posted on 08/21/2006 at 9:10 PM