Search code examples
ruby-on-railsformsmodelsmatching

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.


Solution

  • 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.