Ariel
Ariel

Reputation: 3611

Rails nested attributes not being updated

I'm building my first Rails app and one of the important features in it is having users who speak and/or want to learn languages. In the user edit profile page, I allow him/her to choose what languages he/she speaks and/or wants to learn from a list (I'm using ryanb's nested_form gem):

enter image description here

There are 3 models involved in this: User, Speaks, Language

The languages table is just a table with languages of the world, it doesn't change. It consists basically of ISO codes for languages and their names. I populate it by running a script that reads from the official file I downloaded. Still I was simply using Rails defaults, so the table had an id column, and it was all working fine.

Then I decided to make a change and remove the id column, because it didn't make any sense anyway. I want my app to be up to date with the ISO list. I want the ISO code to identify the languages, not a meaningless id. I want to use

user.speaks.create!(language_id: "pt", level: 6)

instead of

user.speaks.create!(language_id: 129, level: 6)

I know it's unlikely that the ISO list will change but, if it does, I want to simply run my script again with the new file and not worry if the id column will still match the same ISO code as before. So I made the change. Now I can use user.speaks.create the way I want and the association works perfectly in the console. The problem is my form simply isn't working anymore. The data is sent but I don't understand the logs. They show a bunch of SELECTS but no INSERTS or UPDATES, I don't get why. Does anybody have any idea?

Here are my models:

class User < ActiveRecord::Base
  attr_accessible ..., :speaks, :speaks_attributes, :wants_to_learn_attributes

  has_many :speaks, :class_name => "Speaks", :dependent => :destroy
  has_many :speaks_languages, :through => :speaks, :source => :language #, :primary_key => "iso_639_1_code"

  has_many :wants_to_learn, :class_name => "WantsToLearn", :dependent => :destroy
  has_many :wants_to_learn_languages, :through => :wants_to_learn, :source => :language #, :primary_key => "iso_639_1_code"

  ...

  accepts_nested_attributes_for :speaks #, :reject_if => :speaks_duplicate, :allow_destroy => true
  accepts_nested_attributes_for :wants_to_learn #, :reject_if => :wants_to_learn_duplicate, :allow_destroy => true

  # EDIT 1: I remembered these pieces of code silenced errors, so I commented them out

...
end

class Speaks < ActiveRecord::Base
  self.table_name = "speak"
  attr_accessible :language, :language_id, :level
  belongs_to :user
  belongs_to :language

  validates :user, :language, :level, presence: true
  ...
end

#EDIT 4:

class WantsToLearn < ActiveRecord::Base
  self.table_name = "want_to_learn"
  attr_accessible :language, :language_id
  belongs_to :user
  belongs_to :language

  validates :user, :language, presence: true

  ...
end

class Language < ActiveRecord::Base
  attr_accessible :iso_639_1_code, :name_en, :name_fr, :name_pt

  has_many :speak, :class_name => "Speaks"
  has_many :users_who_speak, :through => :speak, :source => :user

  has_many :want_to_learn, :class_name => "WantsToLearn"
  has_many :users_who_want_to_learn, :through => :want_to_learn, :source => :user
end

Controller:

def update
    logger.debug params
    if @user.update_attributes(params[:user])
        @user.save
        flash[:success] = "Profile updated"
        sign_in @user
        redirect_to :action => :edit
    else
        render :action => :edit
    end
end

View:

<%= nested_form_for(@user, :html => { :class => "edit-profile-form"} ) do |f| %>
      <%= render 'shared/error_messages' %>
      <table border="0">
        <tr><td colspan="2"><h2 id="languages" class="bblabla">Languages</h2></td></tr>
        <tr>
          <td><span>Languages you speak</span></td>
          <td class="languages-cell">
            <div id="speaks">
            <%= f.fields_for :speaks, :wrapper => false do |speaks| %>
              <div class="fields">
                <%= speaks.select(:language_id,
                                    Language.all.collect {|lang| [lang.name_en, lang.id]},
                                    { :selected => speaks.object.language_id, :include_blank => false },
                                    :class => 'language') %>
                <%= speaks.label :level, "Level: " %>
                <%= speaks.select(:level, Speaks.level_options, { :selected => speaks.object.level }, :class => 'level') %>
                <%= speaks.link_to_remove raw("<i class='icon-remove icon-2x'></i>"), :class => "remove-language" %>
              </div>
            <% end %>
            </div>
          <p class="add-language"><%= f.link_to_add "Add language", :speaks, :data => { :target => "#speaks" } %></p>
          </td>
        </tr>
        ...

Log:

Started PUT "/users/1" for 127.0.0.1 at 2013-07-19 08:41:16 -0300
Processing by UsersController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"ZmaU9...", "user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374234067848"=>{"language_id"=>"en", "level"=>"5", "_destroy"=>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false", "id"=>"1"}}, "home_location_attributes"=>{"google_id"=>"7789d9...", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative_area_level_1"=>"Rio de Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}, "commit"=>"Save changes", "id"=>"1"}
  [1m[35mUser Load (0.3ms)[0m  SELECT "users".* FROM "users" WHERE "users"."remember_token" = 'bjdvI...' LIMIT 1
  [1m[36mUser Load (0.2ms)[0m  [1mSELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1[0m  [["id", "1"]]
{"utf8"=>"✓", "_method"=>"put", "authenticity_token"=>"ZmaU9W...", "user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374234067848"=>{"language_id"=>"en", "level"=>"5", "_destroy"=>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false", "id"=>"1"}}, "home_location_attributes"=>{"google_id"=>"7789d9...", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative_area_level_1"=>"Rio de Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}, "commit"=>"Save changes", "action"=>"update", "controller"=>"users", "id"=>"1"}
  [1m[35m (0.1ms)[0m  BEGIN
  [1m[36mWantsToLearn Load (0.2ms)[0m  [1mSELECT "want_to_learn".* FROM "want_to_learn" WHERE "want_to_learn"."user_id" = 1 AND "want_to_learn"."id" IN (1)[0m
  [1m[35mLocation Load (0.3ms)[0m  SELECT "locations".* FROM "locations" WHERE "locations"."google_id" = '7789d...' AND "locations"."latitude" = '-22.9035393' AND "locations"."longitude" = '-43.20958689999998' AND "locations"."city" = 'Rio de Janeiro' AND "locations"."neighborhood" = '' AND "locations"."administrative_area_level_1" = 'Rio de Janeiro' AND "locations"."administrative_area_level_2" = '' AND "locations"."country_id" = 'BR' LIMIT 1
  [1m[36mUser Exists (40.0ms)[0m  [1mSELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]') AND "users"."id" != 1) LIMIT 1[0m
  [1m[35m (96.7ms)[0m  UPDATE "users" SET "remember_token" = 'd0pb...', "updated_at" = '2013-07-19 11:41:16.808422' WHERE "users"."id" = 1
  [1m[36m (28.7ms)[0m  [1mCOMMIT[0m
  [1m[35m (0.1ms)[0m  BEGIN
  [1m[36mUser Exists (0.3ms)[0m  [1mSELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]') AND "users"."id" != 1) LIMIT 1[0m
  [1m[35m (0.3ms)[0m  UPDATE "users" SET "remember_token" = 'gKlW...', "updated_at" = '2013-07-19 11:41:17.072654' WHERE "users"."id" = 1
  [1m[36m (0.4ms)[0m  [1mCOMMIT[0m
  Rendered shared/_error_messages.html.erb (0.0ms)
  [1m[35mSpeaks Load (0.3ms)[0m  SELECT "speak".* FROM "speak" WHERE "speak"."user_id" = 1
  [1m[36mWantsToLearn Load (0.2ms)[0m  [1mSELECT "want_to_learn".* FROM "want_to_learn" WHERE "want_to_learn"."user_id" = 1[0m
  [1m[35mLanguage Load (0.3ms)[0m  SELECT "languages".* FROM "languages" 
  [1m[36mCountry Load (0.3ms)[0m  [1mSELECT "countries".* FROM "countries" WHERE "countries"."iso_3166_code" = 'BR' LIMIT 1[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "languages".* FROM "languages" 
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "languages".* FROM "languages" [0m
  Rendered users/edit.html.erb within layouts/application (39.8ms)
  Rendered layouts/_shim.html.erb (0.0ms)
  Rendered layouts/_header.html.erb (1.1ms)
  Rendered layouts/_footer.html.erb (0.2ms)
Completed 200 OK in 576ms (Views: 160.7ms | ActiveRecord: 168.7ms)

Hope someone has an insight cause I've been looking all over the internet for the past 2 days with no luck. Thanks in advance!

EDIT 1

I placed the accepts_nested_attributes_for lines after the associations were made, as suggested by ovatsug25, but it didn't seem to make any change. I remembered, however, that there were some options in the User model that silenced errors, which of course hinders the debugging, so I commented these options out. Now I have the following error:

PG::Error: ERROR:  operator does not exist: character varying = integer
LINE 1: ...M "languages"  WHERE "languages"."iso_639_1_code" = 0 LIMIT ...
                                                             ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT  "languages".* FROM "languages"  WHERE "languages"."iso_639_1_code" = 0 LIMIT 1

I have NO IDEA why Rails is trying to select a language with pk = 0. Even if the pk WAS an integer this wouldn't make sense (would it???) since the default id starts from 1. And even if it started from zero, why would it be trying to select it anyway? Where is this zero comming from?? And I can't "add explicit type casts". The pk is a string and will never be 0 or '0' for that matter. This query doesn't make sense and simply isn't supposed to happen!

EDIT 2

I tried to update the attributes in the console and got the following:

irb(main):006:0> ariel = User.find(1)
  User Load (101.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
=> #<User id: 1, first_name: "Ariel", last_name: "Pontes", ...>
irb(main):007:0> params = {"user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374444891951"=>{"language_id"=>"en", "l
evel"=>"5", "_destroy"=>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false"}}, "home_location_attributes"=>{"google_id"=>"778...c5a", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative
_area_level_1"=>"Rio de Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}}
=> {"user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374444891951"=>{"language_id"=>"en", "level"=>"5", "_destroy"=
>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false"}}, "home_location_attributes"=>{"google_id"=>"778...c5a", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative_area_level_1"=>"Rio de
 Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}}
irb(main):008:0> ariel.update_attributes(params[:user])
   (0.1ms)  BEGIN
  User Exists (0.5ms)  SELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]') AND "users"."id" != 1) LIMIT 1
   (24.9ms)  UPDATE "users" SET "remember_token" = '0tv...Cw', "updated_at" = '2013-07-22 15:45:30.705217' WHERE "users"."id" = 1
   (54.3ms)  COMMIT
=> true
irb(main):009:0> 

Basically, it only updates the remember_token and updated_at for some reason.

EDIT 3

I tried to update only the spoken languages and it worked:

irb(main):012:0> ariel.update_attributes({"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374444891951"=>{"language_id"=>"e
n", "level"=>"5", "_destroy"=>"false"}}})
   (0.2ms)  BEGIN
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
  Language Load (0.8ms)  SELECT "languages".* FROM "languages" WHERE "languages"."iso_639_1_code" = 'pt' LIMIT 1
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
  Language Load (0.2ms)  SELECT "languages".* FROM "languages" WHERE "languages"."iso_639_1_code" = 'en' LIMIT 1
  User Exists (0.2ms)  SELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]') AND "users"."id" != 1) LIMIT 1
   (0.2ms)  UPDATE "users" SET "remember_token" = 'MYh5X1XoF6OsVIo3rhDNzQ', "updated_at" = '2013-07-22 22:05:08.198025' WHERE "users"."id" = 1
  SQL (42.9ms)  INSERT INTO "speak" ("created_at", "language_id", "level", "updated_at", "user_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["created_at", Mo
n, 22 Jul 2013 22:05:08 UTC +00:00], ["language_id", "pt"], ["level", 6], ["updated_at", Mon, 22 Jul 2013 22:05:08 UTC +00:00], ["user_id", 1]]
  SQL (0.4ms)  INSERT INTO "speak" ("created_at", "language_id", "level", "updated_at", "user_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["created_at", Mon
, 22 Jul 2013 22:05:08 UTC +00:00], ["language_id", "en"], ["level", 5], ["updated_at", Mon, 22 Jul 2013 22:05:08 UTC +00:00], ["user_id", 1]]
   (14.7ms)  COMMIT
=> true

I'm starting to fear it may be a case of witchcraft.

PS: Does anybody know why it loads the User 3 times? Seems rather pointless and wasteful.

Upvotes: 2

Views: 2742

Answers (2)

Charles Treatman
Charles Treatman

Reputation: 558

The biggest clue is this error that caught your eye:

HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT  "languages".* FROM "languages"  WHERE "languages"."iso_639_1_code" = 0 LIMIT 1

If you're providing a string value for a model attribute, but the underlying database column is a numeric column, Rails will try to convert the string value to the appropriate numeric type. So, if the underlying column is of type integer, the string input will be interpreted as an integer, using String#to_i. If the string doesn't start with a number, it will be converted to 0.

The Rails console (rails c) can be a useful tool for debugging issues like this. In this case, on the console, you can run WantsToLearn.columns_hash['language_id'].type to see what type Rails thinks it should be using for that attribute. Of course, you can also just as easily check the migrations.

Upvotes: 2

ovatsug25
ovatsug25

Reputation: 8616

I used to have a problem like this and solved it by segreating the accepts_attributes_for calls to the very bottom after all associations and accessible attributes have been declared. (I also merged attr_accesible into one call. I think ryanb says something in this video about the order of the calls. http://railscasts.com/episodes/196-nested-model-form-revised?view=asciicast.

Makes sense? No. But it worked for me.

Upvotes: 0

Related Questions