Zach
Zach

Reputation: 1243

How Do I Update Two Models in Rails Simultaneously?

Let's say I have an app that matches two People against each other, kind of like "hot or not."

I have my matchup view pulling random users out of the database and matching them up against each other. When a user votes for one of the people I would like to increase their :wins and :matches_played by 1 but I would also like to increase the losers :matches_played by 1 so that I can calculate rankings.

Is there is a way to use a form for each person to accomplish this or do I need a model in between, something like Match that has two columns for the winner and loser, and if so how would this work.

Upvotes: 1

Views: 566

Answers (2)

iwasrobbed
iwasrobbed

Reputation: 46713

Doing it only using a People model (the "easy" way):

  • Pro's: It's easy
  • Con's: It's easy for people to inflate their "wins" by forging the form's parameters using Firebug or Chrome's Inspection tool

Assuming you have these routes:

new_person  GET     /people/new(.:format)        {:action=>"new", :controller=>"people"}
edit_person GET     /people/:id/edit(.:format)   {:action=>"edit", :controller=>"people"}
person      GET     /people/:id(.:format)        {:action=>"show", :controller=>"people"}
            PUT     /people/:id(.:format)        {:action=>"update", :controller=>"people"}
            DELETE  /people/:id(.:format)        {:action=>"destroy", :controller=>"people"}

HAML Version of the view code:

- # Person A form
= form_tag(person_path(@personA, :loser_id => @personB), :method => :put) do
  = submit_tag "Vote for Person A"

- # Person B form
= form_tag(person_path(@personB, :loser_id => @personA), :method => :put) do
  = submit_tag "Vote for Person B"

ERB Version:

<%# Person A form %>
<% form_tag(person_path(@personA, :loser_id => @personB), :method => :put) do %>
  <%= submit_tag "Vote for Person A" %>
<% end %>

<%# Person B form %>
<% form_tag(person_path(@personB, :loser_id => @personA), :method => :put) do %>
  <%= submit_tag "Vote for Person B" %>
<% end %>

Then in your controller for the update action you could do:

def update
  People.transaction do
    winner = People.find(params[:id])
    loser = People.find(params[:loser_id])

    # Increment the winner
    winner.increment! :matches_played
    winner.increment! :wins

    # Increment the loser
    loser.increment! :matches_played
  end

  respond_to do |format|
    format.html { redirect_to new_match_path }
  end
end

As someone pointed out in the comments, you should probably wrap this in what is called a transaction so that it's only permanently in the database if everything within the transaction saves successfully.


Even though I'm showing you the method above, I'd still recommend using a Match model. Ultimately, using that extra model will help you validate the following things so people can't game your system.

  • User's can only vote once per match
  • User can't vote on a match involving them
  • User can't forge a form to inflate their wins
  • Separate's concerns as far as Models go and keeps your data more organized and clear for analytics purposes

Yes, this all requires more work (which is why I'm not showing code for it), but it may be something you want to do if you're concerned about any of the above.

Upvotes: 3

andrewpurcell
andrewpurcell

Reputation: 309

You should write your form submission to submit both user ids to your controller, with an indication of the winning user. Then you can operate on both and save them to the db.

winner = People.find(params[:winner_id])
loser  = People.find(params[:loser_id])
winner.matches_played += 1
winner.wins += 1
loser.matches_played += 1
winner.save!
loser.save!

Upvotes: 0

Related Questions