Nested Properties
Save data in associated model objects through the parent.
When you're starting out as a Wheels developer, you are probably amazed at the simplicity of a model's CRUD methods. But then it all gets quite a bit more complex when you need to update records in multiple database tables in a single transaction.
Nested properties in Wheels makes this scenario dead simple. With a configuration using the nestedProperties() function in your model's init() method, you can save changes to that model and its associated models in a single call with save(), create(), or update().
One-to-One Relationships with Nested Properties
Consider a user model that has one profile:
<!--- models/User.cfc --->
<cfcomponent extends="Model">
<cffunction name="init">
<cfset hasOne("profile")>
<cfset nestedProperties(associations="profile")>
</cffunction>
</cfcomponent>
By adding the call to nestedProperties() in the model, you can create both the user object and the profile object in a single call to create().
Setting up Data for the user Form in the Controller
First, in our controller, let's set the data needed for our form:
<!--- In controllers/User.cfc --->
<cffunction name="new">
<cfset var newProfile = model("profile").new()>
<cfset user = model("user").new(profile=newProfile)>
</cffunction>
Because our form will also expect an object called profile nested within the user object, we must create a new instance of it and set it as a property in the call to user.new().
Also, because we don't intend on using the particular newProfile object set in the first line of the action, we can var scope it to clearly mark our intentions for its use.
If this were an edit action calling an existing object, our call would need to look similar to this:
<!--- In controllers/User.cfc --->
<cffunction name="edit">
<cfset user = model("user").findByKey(key=params.key, include="profile")>
</cffunction>
Because the form will also expect data set in the profile property, you must include that association in the finder call with the include argument.
Building a Form for Posting Nested Properties
For this example, our form at views/users/new.cfm will end up looking like this:
#startFormTag(action="create")#
<!--- Data for user model --->
#textField(label="First Name", objectName="user", property="firstName")#
#textField(label="Last Name", objectName="user", property="lastName")#
<!--- Data for associated profile model --->
#textField(
label="Twitter Handle",
objectName="user",
association="profile",
property="twitterHandle"
)#
#textArea(
label="Biography",
objectName="user",
association="profile",
property="bio"
)#
<div>#submitTag(value="Create")#</div>
#endFormTag()#
Of note are the calls to form helpers for the profile model, which contain an extra argument for association. This argument is available for all object-based form helpers. By using the association argument, Wheels will name the form field in such a way that the properties for the profile will be nested within an object in the user model.
Take a minute to read that last statement again. OK, let's move on to the action that handles the form submission.
Saving the Object and Its Nested Properties
You may be surprised to find out that our standard create action does not change at all from what you're used to.
<!--- In controllers/Users.cfc --->
<cffunction name="create">
<cfset user = model("user").new(params.user)>
<cfif user.save()>
<cfset flashInsert(success="The user was created successfully.")>
<cfset redirectTo(controller=params.controller)>
<cfelse>
<cfset renderPage(action="new")>
</cfif>
</cffunction>
When calling user.save() in the example above, Wheels takes care of the following:
- Saves the data passed into the
usermodel. - Sets a property on
usercalled profile with theprofiledata stored in an object. - Saves the data passed into that
profilemodel. - Wraps all calls in a transaction in case validations on any of the objects fail or something wrong happens with the database.
For the edit scenario, this is what our update action would look like (which is very similar to create):
<!--- In controllers/Users.cfc --->
<cffunction name="update">
<cfset user = model("user").findByKey(params.user.id)>
<cfif user.update(params.user)>
<cfset flashInsert(success="The user was updated successfully.")>
<cfset redirectTo(action="edit")>
<cfelse>
<cfset renderPage(action="edit")>
</cfif>
</cffunction>
One-to-Many Relationships with Nested Properties
Nested properties work with one-to-many associations as well, except now the nested properties will contain an array of objects instead of a single one.
In the user model, let's add an association called addresses and also enable it as nested properties.
<!--- models/User.cfc --->
<cfcomponent extends="Model">
<cffunction name="init">
<cfset hasOne("profile")>
<cfset hasMany("addresses")>
<cfset nestedProperties(
associations="profile,addresses",
allowDelete=true
)>
</cffunction>
</cfcomponent>
In this example, we have added the addresses association to the call to nestedProperties().
Setting up Data for the user Form in the Controller
Setting up data for the form is similar to the one-to-one scenario, but this time the form will expect an array of objects for the nested properties instead of a single object.
In this example, we'll just put one new address in the array.
<!--- In controllers/Users.cfc --->
<cffunction name="new">
<cfset var newAddresses = [ model("address").new() ]>
<cfset user = model("user").new(addresses=newAddresses)>
</cffunction>
In the edit scenario, we just need to remember to call the include argument to include the array of addresses saved for the particular user:
<!--- In controllers/Users.cfc --->
<cffunction name="edit">
<cfset user = model("user").findByKey(key=params.key, include="addresses")>
</cffunction>
Building the Form for the One-to-Many Association
This time, we'll add a section for addresses on our form:
#startFormTag(action="create")#
<!--- Data for `user` model --->
<fieldset>
<legend>User</legend>
#textField(label="First Name", objectName="user", property="firstName")#
#textField(label="Last Name", objectName="user", property="lastName")#
</fieldset>
<!--- Data for `address` models --->
<fieldset>
<legend>Addresses</legend>
<div id="addresses">
#includePartial(user.addresses)#
</div>
</fieldset>
<div>#submitTag(value="Create")#</div>
#endFormTag()#
In this case, you'll see that the form for addresses is broken into a partial. (See the chapter on Partials for more details.) Let's take a look at that partial.
The _address Partial
Here is the code for the partial at views/users/_address.cfm. Wheels will loop through each address in your nested properties and display this piece of code for each one.
<div class="address">
#textField(
label="Street",
objectName="user",
association="addresses",
position=arguments.current,
property="address1"
)#
#textField(
label="City",
objectName="user",
association="addresses",
position=arguments.current,
property="city"
)#
#textField(
label="State",
objectName="user",
association="addresses",
position=arguments.current,
property="state"
)#
#textField(
label="Zip",
objectName="user",
association="addresses",
position=arguments.current,
property="zip"
)#
</div>
Because there can be multiple addresses on the form, the form helpers require an additional argument for position. Without having a unique position identifier for each address, Wheels would have no way of understanding which state field matches with which particular address, for example.
Here, we're passing a value of arguments.current for position. This value is set automatically by Wheels for each iteration through the loop of addresses.
Auto-saving a Collection of Child Objects
Even with a complex form with a number of child objects, Wheels will save all of the data through its parent's save(), update(), or create() methods.
Basically, your typical code to save the user and its addresses is exactly the same as the code demonstrated in the Saving the Object and Its Nested Properties section earlier in this chapter.
Writing the action to save the data is clearly the easiest part of this process!
Transactions are Included by Default
As mentioned earlier, Wheels will automatically wrap your database operations for nested properties in a transaction. That way if something goes wrong with any of the operations, the transaction will rollback, and you won't end up with incomplete saves.
See the chapter on Transactions for more details.
Many-to-Many Relationships with Nested Properties
We all enter the scenario where we have "join tables" where we need to associate models in a many-to-many fashion. Wheels makes this pairing of entities simple with some form helpers.
Consider the many-to-many associations related to customers, publications, and subscriptions, straight from the Associations chapter.
<!--- models/Customer.cfc --->
<cfcomponent extends="Model">
<cffunction name="init">
<cfset hasMany(name="subscriptions", shortcut="publications")>
</cffunction>
</cfcomponent>
<!--- models/Publication.cfc --->
<cfcomponent extends="Model">
<cffunction name="init">
<cfset hasMany("subscriptions")>
</cffunction>
</cfcomponent>
<!--- models/Subscription.cfc --->
<cfcomponent extends="Model">
<cffunction name="init">
<cfset belongsTo("customer")>
<cfset belongsTo("publication")>
</cffunction>
</cfcomponent>
When it's time to save customers' subscriptions in the subscriptions join table, one approach is to loop through data submitted by checkBoxTag()s from your form, populate subscription model objects with the data, and call save(). This approach is valid, but it is quite cumbersome. Fortunately, there is a simpler way.
Setting up the Nested Properties in the Model
Here is how we would set up the nested properties in the customer model for this example:
<!--- models/Customer.cfc --->
<cfcomponent extends="Model">
<cffunction name="init">
<!--- Associations --->
<cfset hasMany(name="subscriptions", shortcut="publications")>
<!--- Nested properties --->
<cfset nestedProperties(
associations="subscriptions",
allowDelete=true
)>
</cffunction>
</cfcomponent>
Setting up Data for the customer Form in the Controller
Let's define the data needed in an edit action in the controller at controllers/Customers.cfc.
<!--- In `controllers/Customers.cfc` --->
<cffunction name="edit">
<cfset customer = model("customer").findByKey(
key=params.key,
include="subscriptions"
)>
<cfset publications = model("publication").findAll(order="title")>
</cffunction>
For the view, we need to pull the customer with its associated subscriptions included with the include argument. We also need all of the publications in the system for the user to choose from.
Building the Many-to-Many Form
We can now build a series of check boxes that will allow the end user choose which publications to assign to the customer.
The view template at views/customers/edit.cfm is where the magic happens. In this view, we will have a form for editing the customer and check boxes for selecting the customer's subscriptions.
<cfparam name="customer">
<cfparam name="publications" type="query">
<cfoutput>
#startFormTag(action="update")#
<fieldset>
<legend>Customer</legend>
#textField(
label="First Name",
objectName="customer",
property="firstName"
)#
#textField(
label="Last Name",
objectName="customer",
property="lastName"
)#
</fieldset>
<fieldset>
<legend>Subscriptions</legend>
<cfloop query="publications">
#hasManyCheckBox(
label=publications.title,
objectName="customer",
association="subscriptions",
keys="#customer.key()#,#publications.id#"
)#
</cfloop>
</fieldset>
<div>
#hiddenField(objectName="customer", value="id")#
#submitTag()#
</div>
#endFormTag()#
</cfoutput>
The main point of interest in this example is the <fieldset> for Subscriptions, which loops through the query of publications and uses the hasManyCheckBox() form helper. This helper is similar to checkBox() and checkBoxTag(), but it is specifically designed for building form data related by associations. (Note that checkBox() is primarily designed for columns in your table that store a single true/false value, so that is the big difference.)
Notice that the objectName argument passed to hasManyCheckBox() is the parent customer object and the associations argument contains the name of the related association. Wheels will build a form variable named in a way that the customer object is automatically bound to the subscriptions association.
The keys argument accepts the foreign keys that should be associated together in the subscriptions join table. Note that these keys should be listed in the order that they appear in the database table. In this example, the subscriptions table in the database contains a composite primary key with columns called customerid and publicationid, in that order.
How the Form Submission Works
Handling the form submission is the most powerful part of the process, but like all other nested properties scenarios, it involves no extra effort on your part.
You'll notice that this example update action is fairly standard for a Wheels application:
<cffunction name="update">
<!--- Load customer object --->
<cfset customer = model("customer").findByKey(params.customer.id)>
<!--- If update is successful, generate success message
and redirect back to edit screen --->
<cfif customer.update(params.customer)>
<cfset redirectTo(
action="edit",
key=customer.id,
success="#customer.firstName# #customer.lastName# record updated successfully."
)>
<!--- If update fails, show form with errors --->
<cfelse>
<cfset renderPage(action="edit")>
</cfif>
</cffunction>
In fact, there is nothing special about this. But with the nested properties defined in the model, Wheels handles quite a bit when you save the parent customer object:
- Wheels will update the
customerstable with any changes submitted in the Customers<fieldset>. - Wheels will add and remove records in the
subscriptionstable depending on which check boxes are selected by the user in the Subscriptions<fieldset>. - All of these database queries will be wrapped in a transaction. If any of the above updates don't pass validation or if the database queries fail, the transaction will roll back.
Comments
Read and submit questions, clarifications, and corrections about this chapter.

I'm following through the first section: One-to-One Relationships with Nested Properties
I have an app running with the two objects: user, profile. And all the code model, view and controller code is setup as detailed. I'm using Wheels 1.1 Final.
I am facing two issues:
a) when I edit a user and update the profile, a new record is created in the profile table. This is not the expected behaviour as the original profile associated with the user should be updated, rather than new one created.
b) There is a typo in the update() code
It should read:
<cfset user = model("user").findByKey(params.key)>
rather than
<cfset user = model("user").findByKey(params.user.id)>
params.user.id is not passed through by the form.
Any help would be much appreciated.
Indy,
A new record will be created when you initially generate the object with new() instead of a finder like findOne() or findByKey().
In order to pass user.id in params, add a #hiddenField(objectName="user", property="id")# in your form.
Also, following my earlier comment, when I set the allowDelete to true in the user model for the nestedProperty(), the profile record is not deleted when the user is deleted.
Now I'm working on: One-to-Many Relationships with Nested Properties
Have set it up exactly as detailed.
The address is created correctly.
It is updated correctly.
But It is not deleted (despite allowDelete=true in the user model).
Indy,
Setting allowDelete=true in your nested property does not enable any sort of automatic deletion. It only gives you the ability to manually set the _delete property to true to delete a child through the parent (or to use a helper like hasManyCheckBox(), which also just sets _delete to true).
In order to have Wheels handle automatic dependency deletion, refer to the "Dependencies" section of the Associations chapter:
http://cfwheels.org/docs/1-1/chapter/associations
Hi Chris
Thanks for your response and clarifications.
I now understand how delete works. Basically set up dependent="delete" in the model where association is specified. That works as expected now.
However, I'm still not clear about your comment: "A new record will be created when you initially generate the object with new() instead of a finder like findOne() or findByKey()."
The code in my edit function is: <cfset user = model("User").findByKey(key=params.key, include="profile,addresses")>
Not sure where I should be using a finder method? Using this code the view populates the form correctly, including details of the user, profile, and addresses.
When I update, the user record gets updated, address gets updated, but a new record is created for profile (rather than updating the existing one).
Thanks again.
Indy,
I'm not quite sure why it would be creating the profile instead of updating. Are you using the association argument in your profile form helpers? What happens if you <cfdump> the user object after calling findByKey and setProperties in your update action?
Thanks for that. Your comment about setProperties() made me look at the update() code again.
It read: <cfset user = model("User").findByKey(key=params.key)>
If I change that to: <cfset user = model("User").findByKey(key=params.key, include="profile,addresses")>, the profile is updated correctly.
But the address stops updating. So maybe I need to post the code somewhere and seek your feedback. I'm obviously doing something silly somewhere. Doing it late at night is possibly not helping it either.
Chris, I finally got it going. I had commented out putting in the one-to-many association for user-address in the user model, and that is why address wasn't updating.
So both profile and address update correctly, provided I put use:
<cfset user = model("User").findByKey(key=params.key,include="profile")>
in the update() function.
The issue is that when I'm editing a user, there is no reference to the profile ID anywhere in the source code. I think that is why Wheels creates a new record for profile on each update. If I put in a hidden value in my form: <input type="hidden" name="user[profile][id]" value="#user.profile.id#">
that makes the profile update correctly as well.
But I guess the first approach of using an include in the update() function is a better approach.
Thanks for your help.
I see these Nested Properties examples are all for edit forms, but I would like to be able to use this technique for a create form with objects that have a one-to-many relationship. The form I have allows the user to add multiple child objects on the same page as the create form for the parent object (all done with JavaScript). The problem I am running into is wheels doesn't seem to like the fact that I am inserting new child objects (on the form page) without its knowledge so it doesn't know what to do with the unique ID I am auto-populating. Without the parent object existing first, I'm not sure how I could create the child objects first using an AJAX call. Any thoughts?
Would be nice if you could show the DATABASE and all TABLES and fields for your examples be easier to learn. Could you share that in some place? We appreciated. :)