I’ve just started development on a new cfWheels application, and since 1.1.2 has been released, I’ve been meaning to dig down into some of the newer features and functions, such as nested properties. This isn’t a small topic, but once you get the gist of what cfWheels is doing behind the scenes, you may well sit there with your jaw on the floor for a little bit.
So, Nested Properties – where to start? Well, what’s one of the most common things about any ‘relational database based’ (try saying that three times quickly) application? Join tables: updating, deleting and adding those joins – done traditionally, it’s a bit of a chore, let’s be honest.
So how does Wheels do it?
Let’s take a simple, real world example.
You have a Contact. They have multiple email addresses, and that contact might also be classed into multiple categories.
Our email addresses are unique to each contact, so this is a ‘one to many’ relationship. One contact, multiple addresses.
Our contact may be in multiple categories, i.e this could be something like Alumni, Funder etc. the point is with these values, you don’t want to repeat them for each contact – the data is repeated: additionally, these categories are predefined, so having them as a database table is more convienient. So this is a many to many relationship: a Contact may have multiple categories, and each category can encompass multiple contacts.
So let’s look at the models and database table for what we’ve got so far.
contacts (Our main contacts object)
emailaddresses (our email address storage)
categorytypes (our list of categories)
categories (our join table)
<!---Contact.cfc---> <cfcomponent extends="Model" output="false"> <cffunction name="init"> <cfset property(name="createdBy", defaultValue=session.currentuser.id)> <cfset property(name="updatedBy", defaultValue=session.currentuser.id)> <cfset hasMany(name="emailaddresses", dependent="deleteAll")> <cfset hasMany(name="categories", dependent="deleteAll")> <cfset nestedProperties(associations="emailaddresses,categories", allowDelete=true)> </cffunction> </cfcomponent> <!---EmailAddress.cfc---> <cfcomponent extends="Model" output="false"> <cffunction name="init"> <cfset belongsTo("contact")> </cffunction> </cfcomponent> <!---CategoryType.cfc---> <cfcomponent extends="Model" output="false"> <cffunction name="init"> <cfset hasMany(name="categories")> </cffunction> </cfcomponent> <!---Category.cfc---> <cfcomponent extends="Model" output="false"> <cffunction name="init"> <cfset belongsTo("contact")> <cfset belongsTo("categoryType")> </cffunction> </cfcomponent>
So now we’ve got the basics setup, let’s look at actually using these associations in a meaningful way.
Adding a new contact
My Contacts.cfc controller will be handling all the CRUD operations for the contact model. Because of our nested properties, when we create the initial contacts object, we need to also create the email address and categories objects *as part of* the contacts object.
<!---Contacts.cfc---> <cffunction name="init"> <cfset filters(through="getCategoryTypes", only="add,edit,update")> <cfset filters(through="getCurrentContact", only="view,edit,update")> <cfset verifies(only="getCurrentContact", params="key", paramsTypes="integer")> </cffunction> <cffunction name="add" hint="Add a New Contact"> <cfset var newEmailaddress=model("emailaddress").new()> <cfset var newCategory=model("category").new()> <cfset contact=model("contact").new(emailaddresses=newEmailaddress,categories=newCategory)> <cfset renderPage(action="edit")> </cffunction> <cffunction name="update"> <cfset contact.update(params.contact)> <cfif contact.hasErrors()> <cfset renderPage(action="edit")> <cfelse> <cfset flashInsert(success="The contact was updated successfully.")> <cfset redirectTo(action="view", key=contact.id)> </cfif> </cffunction> <cffunction name="getCategoryTypes" access="private"> <cfset categoryTypes=model("categoryTypes").findAll()> </cffunction> <cffunction name="getCurrentContact" access="private"> <cfset contact=model("contact").findone(where="id=#params.key#", include="emailaddresses,categories")> </cffunction>
Where possible, I try and reuse the edit/add forms, as this means you’re not repeating yourself (hence the renderPage bit).
You’ll notice two private functions: one just gets the predefined values for Categories, and the other gets the Current Contact, and *includes* our email addresses and categories. By using a filter, I don’t need to repeat myself later when we’ve got the view functions, and I also don’t need an entry for edit or view. Additionally, I’m verifying the getCurrentContact method, checking it always has params.key as an integer.
Also, not that I’ve created newCategory and newEmailAddress as private vars (this is just to keep it self contained), but i’ve also created them as one dimensional arrays: the new Objects go in the first position in these arrays – keep this in the back of your mind.
My main edit form will end up looking something like this:
<!---Edit.cfm---> <cfif params.action EQ "add"> <h2>Add a New Contact</h2> #startFormTag(action="create")# <cfelse> <h2>Editing Contact</h2> #startFormTag(action="update", key=params.key)# </cfif> #errorMessagesFor("contact")# <!--- Basic info for contact ---> #textField(objectName="contact", property="firstname", label="First Name *", class="required")# #textField(objectName="contact", property="middlename", label="Middle Name")# <!--- Snip Etc…---> <!--- Email Addresses ---> #includePartial(contact.emailaddresses)# <!--- Categories ----> <cfloop query="categoryTypes"> #hasManyCheckBox(label=name, objectName="contact", association="categories", keys="#contact.key()#,#categoryTypes.id#")# </cfloop> #submitTag()# #endFormTag()#
Immediately, there will probably be two bits which raise an eyebrow: 1) the ‘includePartial’ call for email addresses, and 2) the HasManyCheckBox.
Let’s take the categories first as it’s a bit simpler.
The categoryTypes loop loops over each checkbox. Wheels then checks for an association named categories on the contact object. A composite key is used to look for the existence of the record in the join table. If it exists, it’ll appear checked. The best part is, that as we’re looping over the categoryTypes query, we’ve got access to the actual values of the table, so in this example, the categoryTypes.name column will appear as the label. Nice.
The Email Address includePartial:
This is ‘slightly’ more complicated, but not much. If you remember, we created the contacts object with two additional nested objects (in arrays).
The partial loops over the contacts.emailaddresses entry, and seeing an array, loops over that too. Cunning.
<!---_emailaddress.cfm---> <cfoutput> #textField(objectName="contact", association="emailaddresses", position=arguments.current, property="email", label="Email Address", size=62, class="email")# #select(objectName="contact", association="emailaddresses", position=arguments.current, property="type", label="Type", options="Home,Work")# #checkbox(objectName="contact", association="emailaddresses", position=arguments.current, property="preferred", label="Preferred")# </cfoutput>
So the important part here is the association, and the position. The position is used by Wheels to say which iteration of the loop you’re on, and the association helps point back to the main contact model.