我正在构建我的第一个Rails应用程序,其中一个重要功能是让用户说话和/或想要学习语言。在用户编辑个人资料页面中,我允许他/她选择他/她说话和/或想要从列表中学习的语言(我正在使用ryanb的嵌套_形式宝石):
此处涉及3个模型:User
,Speaks
,Language
languages
表只是一个包含世界语言的表,它不会改变。它基本上由语言及其名称的ISO代码组成。我通过运行从我下载的官方文件中读取的脚本来填充它。我仍然只是使用Rails默认值,因此该表有一个id列,并且一切正常。
然后我决定进行更改并删除id列,因为它无论如何都没有任何意义。我希望我的应用程序能够与ISO列表保持同步。我希望ISO代码能够识别语言,而不是无意义的ID。我想用
user.speaks.create!(language_id: "pt", level: 6)
而不是
user.speaks.create!(language_id: 129, level: 6)
我知道ISO列表不太可能会改变,但如果确实如此,我想简单地用新文件再次运行我的脚本,如果id列仍然匹配与以前相同的ISO代码,请不要担心。所以我做了改变。现在我可以按照我想要的方式使用user.speaks.create
,并且关联在控制台中完美运行。问题是我的表格根本不再起作用了。数据已发送但我不了解日志。他们展示了一堆SELECTS但没有INSERTS或UPDATES,我不明白为什么。有人有任何想法吗?
以下是我的模特:
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
控制器:
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
查看:
<%= 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>
...
日志:
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('ariel@pontes.com') 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('ariel@pontes.com') 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)
希望有人有洞察力,因为过去两天我一直在网上寻找,但没有运气。提前谢谢!
编辑1
按照ovatsug25的建议,我在关联之后放置了accepts_nested_attributes_for
行,但它似乎没有任何改变。然而,我记得User
模型中有一些选项可以解决错误,这当然会妨碍调试,所以我对这些选项进行了评论。现在我有以下错误:
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
我没有IDEA为什么Rails试图选择pk = 0的语言。即使pk是一个整数,这也没有意义(是吧???)因为默认id从1开始。甚至如果从零开始,为什么还要尝试选择呢?这个零来自哪里?我不能“添加显式类型转换”。 pk是一个字符串,对于那个问题永远不会是0或'0'。这个查询没有意义,根本不应该发生!
编辑2
我尝试更新控制台中的属性并获得以下内容:
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('ariel@pontes.com') 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>
基本上,它只会因某种原因更新remember_token
和updated_at
。
编辑3
我尝试只更新口语并且有效:
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('ariel@pontes.com') 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
我开始担心这可能是巫术的情况。
PS:有人知道为什么它会加载用户3次吗?似乎没有意义和浪费。
答案 0 :(得分:2)
最大的线索是引起你注意的错误:
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
如果您为模型属性提供字符串值,但基础数据库列是数字列,则Rails将尝试将字符串值转换为适当的数字类型。因此,如果基础列的类型为整数,则使用String#to_i
将字符串输入解释为整数。如果字符串不以数字开头,则将其转换为0。
Rails console(rails c
)可以成为调试此类问题的有用工具。在这种情况下,在控制台上,您可以运行WantsToLearn.columns_hash['language_id'].type
以查看Rails认为应该为该属性使用的类型。当然,您也可以轻松检查迁移。
答案 1 :(得分:0)
我曾经有这样的问题,并且在声明了所有关联和可访问属性之后,通过将accepts_attributes_for
调用隔离到最底层来解决它。 (我还将attr_accesible
合并为一个电话。我认为ryanb在此视频中说了有关电话顺序的内容。http://railscasts.com/episodes/196-nested-model-form-revised?view=asciicast。
有道理吗?不,但它对我有用。