Multi Model Forms & Validations in Ruby on Rails

September 6th, 2008

Edit: The code has been updated due to the awesome information presented in the comments.

For a project we’ve been working on at Fudge Studios, we had to create four (uurgh) models from one form. After a lot of digging around and some trial and error we came up with a method which works well for us. I thought I’d document it for others as I couldn’t find a lot about it when originally trying do it.

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.

Further Things to Read with Your Eyes

(Possibly) Related Posts

Recommend Me

If you found this post or anything else on this site of any use, then please take the time to recommend me on Working with Rails.

You can follow any responses to this entry through the RSS 2.0 feed. Trackback from your own site.

31 Responses to “Multi Model Forms & Validations in Ruby on Rails”

  1. September 6th, 2008 at 1:36 pm - Craig Says:

    You might have some of your heartache solved with James Golick’s Active Presenter.


  2. September 6th, 2008 at 1:58 pm - S. Brent Faulkner Says:

    sweet! I like the simplicity of this as opposed to a transaction block!


  3. September 6th, 2008 at 3:26 pm - Tobias Luetke Says:

    I’d replace

    unless [@person, @cat, @dog].map(&;:valid?).include?(false)

    with

    if [@person.valid?, @cat.valid?, @dog.valid?].all?


  4. September 6th, 2008 at 3:28 pm - Oleg Andreev Says:

    Try google for “Presenter pattern” :-)

    http://blog.jayfields.com/2007/03/rails-presenter-pattern.html


  5. September 6th, 2008 at 4:04 pm - Chris Says:

    In your original solution you could also use a single ampersand:
    @person.valid? & @cat.valid? & @dog.valid?

    This should force your if statement to evaluate each term


  6. September 6th, 2008 at 4:19 pm - Ethan Vizitei Says:

    Great Post! I’ve run into this problem a couple times before, and this is (in my opinion) a good solution. I’ll be bookmarking this page for future reference; thanks for putting it up.


  7. September 6th, 2008 at 4:36 pm - Philipe Farias Says:

    Good solution!
    I have used something a little different…I saw in the Recipe 18, by Rick Olson (aka Technoweenie), of Advanced Rails Recipe book that he creates a model service class where goes all the stuff for validating and saving (and other fancy things if you need) all models uploaded in the form. This way the controller keeps really thin.


  8. September 6th, 2008 at 4:54 pm - David Says:

    Interesting case. I think this kind of logic can be moved into a dedicated object to make cleaner the controller.

    Just a syntax improvement : if [@person, @cat, @dog].all?(&:valid?)


  9. September 6th, 2008 at 6:47 pm - nicolas Says:

    That’s an elegant way of doing the validations, I would also use a transaction to be extra cautious:
    unless [@person, @cat, @dog].map(&;:valid?).include?(false)
    Person.transaction do
    @person.save!
    @cat.save!
    @dog.save!
    end
    else…


  10. September 6th, 2008 at 7:08 pm - ActsAsFlinn Says:

    Very interesting way to get this done. I’ve not ever needed to assign concurrent objects in a single form but the problem I’ve run into several times before is nested model mass assignment. Good news everyone! An upcoming rails release will sport this very feature (probably 2.2): http://ryandaigle.com/articles/2008/7/19/what-s-new-in-edge-rails-nested-models


  11. September 6th, 2008 at 7:36 pm - Mina Says:

    You really should consider James Golick & Daniel Haran’s ActivePresenter


  12. September 6th, 2008 at 10:38 pm - Jaime Iniesta Says:

    Hey, nice solution. You should also consider using transactions when saving your 3 models or you could end up with a saved dog and a saved person but not a saved cat: just think what would happen if you have a validates_uniqueness_of :name on your Cat model that prohibits the cat being saved just because on that same moment someone saves an equally named cat while you were busy checking if it was valid or not. :)


  13. September 7th, 2008 at 1:53 am - W. Andrew Loe III Says:

    You can do something like this as well:

    if [@person.valid?, @cat.valid?, @dog.valid?].all?


  14. September 7th, 2008 at 5:01 am - coderrr Says:

    how about:

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


  15. September 7th, 2008 at 10:30 am - edek Says:

    You can just put first “bad” snippet in the transaction and use save! instead of save.


  16. September 7th, 2008 at 11:08 am - edek Says:

    Oh, and there’s also a missing transaction even if you use code proposed at this website. This code can handle invalid data, but it will make database incosistency when e.g. your MySQL will run out of space before first and second save (when all models are valid!). It’s not paranoid, it’s just a good programming habit.


  17. September 7th, 2008 at 4:35 pm - Stephen Celis Says:

    Ryan Bates highlighted a solution, awhile back, that would clean up your controller a bit (no need for the multiple “valid?” and “save” calls):

    http://railscasts.com/episodes?search=complex+forms

    More recently, he put together a Github project that has other examples:

    http://github.com/ryanb/complex-form-examples/tree/master


  18. September 7th, 2008 at 5:44 pm - pascal betz Says:

    What about transactions ? You should wrape the calls to save in a transaction if it is a “all or nothing” scenario.
    And no… i did not know that you can pass an array of objects to error_messages_for(). thanks.


  19. September 8th, 2008 at 2:49 am - Tom Says:

    Nice. I wonder if what you think of this

    http://jamesgolick.com/2008/7/28/introducing-activepresenter-the-presenter-library-you-already-know


  20. September 8th, 2008 at 8:16 am - Tom-Eric Says:

    You could also use & instead of && in your if’s.

    So instead of writing if [@person, @cat, @dog].map(&:valid?).include?(false), you could write if @person.valid? & @cat.valid? & @dog.valid?.


  21. September 8th, 2008 at 8:35 am - Matthew Lang Says:

    I didn’t know you could do that either!

    I’m currently working on a project that will require this. Although I haven’t actually reached the coding side of it, but the screens are all laid out and it’s been on the back of my mind since I thought I would need to validate more than one model.

    Great write-up Jim. Looking forward to more gems like this from yourself.


  22. September 8th, 2008 at 9:08 am - David Backeus Says:

    You might want to have a look at the “presenter” pattern as well. Here is a nice implementation called ActivePresenter: http://jamesgolick.com/2008/7/28/introducing-activepresenter-the-presenter-library-you-already-know


  23. September 8th, 2008 at 9:17 am - Jim Neath Says:

    Thanks for all the replies. I didn’t actually know you could do half the stuff proposed in the these comments, so thanks guys.

    I’ll update the article later on today so reflect everything you chaps have said.

    Kudos to you :)


  24. September 8th, 2008 at 9:18 am - Liam Morley Says:

    Is there a nice way to do the same thing for the update method as well as create? I don’t see any alternative to update_attributes that doesn’t save the record…


  25. September 8th, 2008 at 9:53 am - Fadhli Says:

    There’s also another way in the Advanced Rails Recipe book for handling multiple-model in one form. It’s a technique written by Ryan Bates. You can go check it ‘em up too.


  26. September 8th, 2008 at 10:07 am - Liam Morley Says:

    Scratch that. I should look at more than just ‘update’. This works just fine with attributes= in the update method.


  27. September 8th, 2008 at 12:46 pm - Anon Says:

    Hai! That fail is not epic!


  28. September 8th, 2008 at 4:09 pm - Jonathan Says:

    Very nice!


  29. September 8th, 2008 at 4:22 pm - funfun Says:

    very helpfull, thanks


  30. September 8th, 2008 at 5:07 pm - BradfordW Says:

    Yea after seeing this trickle across dzone the first thing that popped into my head was the presenter model that ol’ boy over at (http://jamesgolick.com/2008/7/28/introducing-activepresenter-the-presenter-library-you-already-know) did.

    Glad I’m not the only one drinking the kool aid.


  31. November 6th, 2008 at 8:45 pm - Pepe Gonzales Says:

    Question…
    If all the save! calls are wrapped with a transaction, is running valid? on all models necessary?
    I thought that if, for example, @dog isn’t valid, the save! call will throw an exception, and the whole transaction will be undone (ie. @person and @cat will be destroyed).
    Am I wrong?


Leave a Reply

Jim Neath is a 24 year old Ruby on Rails developer from Manchester, UK. Contact Jim Neath

Recommend Me

Categories

Stalk Me