Multi Model Forms & Validations in Ruby on Rails

Tháng Ba 26, 2009

Source: http://jimneath.org/2008/09/06/multi-model-forms-validations-in-ruby-on-rails/

Background

Say we have 3 models that are all mutually exculise from each other but we need to make sure that all of the models are valid before we save them. The original pplan was to use something similiar to the following:

if @person.save && @cat.save && @dog.save
  // Woohoo!
else
  // Epic fail
end

The problem with this method is: if @person and @dog are valid and save correctly but @dog fails due to validation, then you end up with a person and cat in your database when you don’t want them. This leads to infinite amounts of problems, as rSpec has happily announced on more than one occassion.

The second thing I tried was checking that all the models are valid first then saving them:

if @person.valid? && @cat.valid? && @dog.valid?
  @person.save
  @cat.save
  @dog.save
else
  // Epic fail
end

While this looks good on the controller side, it actually sucks. If person is invalid then the errors are shown on the page as expected, but you don’t to see if any of the other models have any errors because valid? doesn’t get called on them. Aaargh!

The Solution

The View

Did you know you could pass in more than one model to error_messages_for? No, neither did I? What about fields_for? It’s a life saver. Let’s look at an example form for a Person, Cat and a Dog:

<% form_for @person do |f| %>
<%= error_messages_for :object => [@person, @cat, @dog] %>
  <fieldset>
    <legend>Person Details</legend>
    <ol>
      <li>
        <%= f.label :name %>
        <%= f.text_field :name %>
      </li>
    </ol>
  </fieldset>
  <% fields_for :cat do |cat| %>
    <fieldset>
      <legend>Cat Details</legend>
      <ol>
        <li>
          <%= cat.label :breed %>
          <%= cat.text_field :breed %>
        </li>
      </ol>
    </fieldset>
  <% end %>
  <% fields_for :dog do |dog| %>
    <fieldset>
      <legend>Dog Details</legend>
      <ol>
        <li>
          <%= dog.label :breed %>
          <%= dog.text_field :breed %>
        </li>
      </ol>
    </fieldset>
  <% end %>
  <%= f.submit 'Pow!' %>
<% end %>

The main things to look out for here are error_messages_for and fields_for. We pass in an array of the objects we want to display errors for into error_messages_for using :object. This will display all the errors for those models, in our case the person cat and dog errors. Although you need to make sure all the errors for these models are being raised. We’ll look at this later on.

The other thing to take a look at is fields_for. I’ll let the API docs explain this for you:

Creates a scope around a specific model object like form_for, but doesn‘t create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form.

So that’s our views done. On to the controller:

The Controller

This is the way we’ve been coping with the problem of validating all the models. I’m sure other people will have other suggestions, but this is what we’re rocking:

def new
  @person = Person.new
  @cat = Cat.new
  @dog = Dog.new
end

def create
  @person = Person.new(params[:person])
  @cat = Cat.new(params[:cat])
  @dog = Dog.new(params[:dog])

  # Run valid? on each model and check for failures
  if [@person, @cat, @dog].all?(&;:valid?)
    Person.transaction do
      @person.save!
      @cat.save!
      @dog.save!
    end
  else
    // Epic fail
  end
end

The only line that you really need to checkout here is the line that runs valid? on each model and check results:

if [@person, @cat, @dog].all?(&;:valid?)

This line runs through each model and runs the valid? method and checks that all the results are true.

As pointed out in the comments, this line could be replaced with:

if @person.valid? & @cat.valid? & @dog.valid?

The Person.transaction block makes sure that if one of the models fails to save then the other models aren’t saved as well. This stops you ending up with random saved models that shouldn’t be there.

Bosh.

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s

%d bloggers like this: