Question

I'm new to Ruby and Rails so there's probably a better approach to what I want to do but I'd appreciate any help understanding why exactly my approach fails rather than what a different approach would look like. I'm using:

  • Ruby 1.8.7
  • Rails 3.2.12
  • Redmine 2.2.3 (Although I don't think is is entirely relevant here)
  • MySQL 5.6

I have a Skin.rb model (skin as in appearance, not organ) and I have one skin for Android environments and different skin for iOS environments. A skin can have zero or one language files associated with it and zero or one graphics files associated with it. The attributes for these skins are displayed in the app\views\skins\index.html.erb view which lists each of the skins:

<% @skins.each do |skin| %>
  <% if skin.device_os == 'android' %>
    <%= content_tag(:h3, 'Android') %>
  <% elsif skin.device_os == 'ios' %>
    <%= content_tag(:h3, 'iOS') %>
  <% end%>

 <table>
    <thead><tr>
      <td>Languages</td>
      <td>Graphics</td>
      <td></td>
      <td></td>
    </tr></thead>

    <tbody>    
      <tr>
        <%= form_for :skin, :url => skins_path do |f| %>
          <td><%= f.collection_select :lang_file, (Attachment.find_by_sql [@lang_file_sql, @current_project.id]), :id, :filename, {:prompt => skin.lang_file.present? ? Attachment.find(skin.lang_file).filename : "Select a languages file"} %></td>
          <td><%= f.collection_select :graphics_pack, (Attachment.find_by_sql [@graphics_pack_sql, @current_project.id]), :id, :filename, {:prompt => skin.graphics_pack.present? ? Attachment.find(skin.graphics_pack).filename : "Select a graphics pack"} %></td>
          <td><%= hidden_field('skin', 'id', {:value => skin.id}) %></td>
          <td><%= f.submit %></td>
        <% end %>
      </tr>
    </tbody>
  </table>
<% end %>

I'd like to be able to update the attributes of either the Android skin or the iOS skin in the index view and have the appropriate record in the skins table updated. However, when I try and update the record a new record is created instead of the relevant record being updated.

The way I'm trying to do this is to pass the updated skin from the index view with its id and updated lang_file and graphics_pack attributes to the skins_controller#create method. The POST as traced by WEBrick looks like this:

Started POST "/skins" for 127.0.0.1 at Tue Feb 25 15:25:04 +0000 2014
Processing by SkinsController#create as HTML
  Parameters: {"authenticity_token"=>"sZWVl8IO1IKRNa/fStps8pUehDcSqQsaN/vpL3BITf8=", "commit"=>"Save
Skin", "utf8"=>"Ô£ô", "skin"=>{"lang_file"=>"6", "graphics_pack"=>"", "id"=>"4"}}

You can see the params[:skin] parameter passed above.

This method uses the new method to create a new Skin object with the attributes passed in params[:skin]. The create method looks as follows (comments refer to WEBrick trace above):

def create
 @skin = Skin.new(params[:skin]) #@skin{ id: => 4, lang_file: => 6, graphics_pack => nil }
 if @skin.save #update skins table if record with skins.id=4 already exists else create new record
   redirect_to :back
 else
   # do error handling stuff
 end
end

As far as I understand, since skin.id is the primary key for the skins table, save works (simplistically) as follows:

  1. There is currently no record with skins.id=4 so a new one is created
  2. There is already a record with skins.id=4 so that record is updated with its attributes set as per those in the POST request.

http://apidock.com/rails/ActiveRecord/Base/save sand rails activerecord save method both suggest I'm doing the right thing but it's not working.

What I observe is that each time I try to configure one of the existing skins, a new skin record is created in the skins table with its skins.id auto-incremented from the last one created. The params[:skin][:id] appears to be ignored.

Can I use the new and save methods to update/create a new record as necessary? How do I do that? I think I'm passing enough information to my SkinsController so I'm expecting the answer to lie in the SkinsController#create method itself.

(As for why I'm doing it this way when there are probably better ways:

  1. My use cases are such that there should already be an Android skin and an iOS skin by the time the user navigates to http://.../skins and
  2. I think it's nice to seamlessly update/create these records with the same bit of code if the language allows it so I'm avoiding the various update specific methods in rails (e.g. update_attributes. Besides, I think they just wrap around save anyway.)

I'd like to understand how my code is failing rather than which other approach may be better.

Was it helpful?

Solution

Use first_or_create

def create
 @skin = Skin.where(:id => params[:skin][:id]).first_or_create #@skin{ id: => 4, lang_file: => 6, graphics_pack => nil }
 if @skin.update_attributes(params[:skin]) #update skins table if record with skins.id=4 already exists else create new record
   redirect_to :back
 else
   # do error handling stuff
 end
end

params[:skin][:id] will get ignored because its protected attribute. You cannot mass assign id

skin = Skin.new(:id => 1, :lang_file => 6) #id will be ignored and autoincremented while saving
skin.id = 3 #this will work. id will be set to 3 
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top