One of the issues in my previous post The Controller Action that sparked some interest is the handling of the creation of multiple Models in one Action. In this post I shall elaborate on this problem in some detail, first considering the cases where in constructing the Dependent Object no data are needed from the querystring/params, and secondly where such data are necessary (and where we have a complicated nested Form). Let’s head right in to an example.
With No Extra Params to Worry About
Your site has Groups and Memberships. Our Business Rule is: when a user creates a Group, she should also be a Member of that Group. Following the Controller Formula, we know the solution in advance:
class GroupsController < ActionController::Base
def create
group = logged_in_user.groups.build(params[:group])
raise SecurityTransgressionError.new unless logged_in_user.can_create?(group)
if group.save
...
end
end
end
This leaves unresolved where to put our Business Rule…
Let’s just put it in the Model as a before_create:
class Group < ActiveRecord::Base
belongs_to :creator, :class_name => 'User'
has_many :memberships
has_many :members, :through => :memberships
before_create :create_first_member
private
def create_first_member
memberships.build(:member => creator)
end
end
Building the Membership as a before_create relies on the fact that objects built in a Proxy will be cascaded (i.e., saved) on creation.
Some Extra Params to Worry About
A more complicated example is where we have a Form that prompts the User for data for the creation of two Models, one Dependent upon the other. Let’s use the example where our Model is a Cyclops and the Dependent Model is an Eyeball. First we need a new Action:
def new
@cyclops = Cyclops.new
end
Pretty Skinny, eh? Very Formulaic too. The corresponding View will look something like this:
<% form_for :cyclops do |c| %>
...
<% fields_for 'cyclops[eyeball_attributes]', @cyclops.eyeball do |e| %>
...
<% end %>
<% end %>
When the User submits this form, params come into our create Action looking like this:
{
'cyclops' => {
'name' => 'Polyphemus',
'eyeball_attributes' => {'color' => 'grey'}
}
}
So what should the create Action look like? Well, we don’t have to think about it, because we’re following the Controller Formula:
def create
cyclops = Cyclops.new(params[:cyclops])
...
if cyclops.save
...
end
end
Two things now need to be implemented in the model. First, @cyclops.eyeball should not be nil even if the Cyclops is brand new. This is because we assume an Eyeball exists when we draw the new Form. A simple way of accomplishing this is the override the getter for eyeball so that it will build an eyeball if none exists:
class Cyclops < ...
has_one :eyeball
def eyeball
@eyeball || build_eyeball
end
end
This still leaves unresolved how to deal with setting the Dependent Model, the Eyeball. Given the way we drew the form above, we need merely implement eyeball_attributes=
:
class Cyclops < ...
...
def eyeball_attributes=(attrs)
eyeball.attributes = attrs
end
end
The eyeball_attribues=
method will get called automatically when the create action passes in params[:cyclops] to the Cyclops initializer. (I wish we didn’t have to call this Attribute eyeball_attributes
, but calling it just eyeball would require too much fancy footwork for my taste)
At this point, the only issue outstanding is how to deal with Validation. An invalid Dependent Model (which cannot be saved) will not make the Parent Model invalid by default when has_one
is used (but it will validate by default when using has_many
–there’s your Principle of Least Surprise for ya!). So it’s easy to imagine a scenario where the User inputs bad data for the Eyeball, good data for the Cyclops, and therefore Rails would save the Cyclops but not the Eyeball, and we’d have a sightless Cyclops with nary an error in sight. The only logical thing to do is add the following Business Rule: a Cyclops is invalid on creation if its Eyeball is invalid. This is simple enough:
class Cyclops < ...
validates_associated :eyeball, :on => :create
end
I love Rails.
About the Author