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 config()
method, you can save changes to that model and its associated models in a single call with save(), create(), or update().
Consider a user
model that has one profile
:
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().
First, in our controller, let's set the data needed for our form:
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:
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.
For this example, our form at app/views/users/new.cfm
will end up looking like this:
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.
You may be surprised to find out that our standard create
action does not change at all from what you're used to.
When calling user.save()
in the example above, Wheels takes care of the following:
Saves the data passed into the user
model.
Sets a property on user
called profile with the profile
data stored in an object.
Saves the data passed into that profile
model.
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
):
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. We know that one user
can have many addresses
. Furthermore, we know that each user
has only one profile
.
In the user
model, let's add an association called addresses
and also enable it as nested properties.
In this example, we have added the addresses
association to the call to nestedProperties().
The addresses
table contains a foreign key to the Users
table called userid
, Now in the addresses
model, let's associate it with its parent User
and also enable it as nested properties.
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 the edit
scenario, we just need to remember to call the include
argument to include the array of addresses saved for the particular user
:
This time, we'll add a section for addresses on our form:
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.
Here is the code for the partial at app/views/users/_address.cfm
. Wheels will loop through each address
in your nested properties and display this piece of code for each one.
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
.
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!
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.
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.
When it's time to save customer
s' 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.
Here is how we would set up the nested properties in the customer
model for this example:
Let's define the data needed in an edit
action in the controller at app/controllers/Customers.cfc
.
For the view, we need to pull the customer
with its associated subscriptions
included with the include
argument. We also need all of the publication
s in the system for the user to choose from.
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 app/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
.
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.
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:
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 customers
table with any changes submitted in the Customers <fieldset>
.
Wheels will add and remove records in the subscriptions
table 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.