Creating Multiple Models in One Action

July 18, 2007 Pivotal Labs

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

Biography

More Content by Pivotal Labs
Previous
Til the End of Time (or Time.now)
Til the End of Time (or Time.now)

Sooner or later, every test-driven developer discovers that they need a superpower - the power to control t...

Next
Sake for Gems Downloads List
Sake for Gems Downloads List

I have a few gems on Rubyforge and I want to track how many of them were downloaded. I found Firefox's sear...

How do you measure digital transformation?

Take the Benchmark