Rails accepts_nested_attributes_for 无法处理 has_many 关系和茧 gem
Rails accepts_nested_attributes_for not working with has_many relationship and cocoon gem
我正在使用 cocoon gem 构建用于创建锦标赛的表单。锦标赛 has_many
游戏。 Cocoon 让我可以动态地向表单中添加更多游戏。
当我调用 @tournament.save
时,它会生成以下错误:
Games team one must exist
Games team two must exist
tournament.rb
class Tournament < ApplicationRecord
has_many :games
accepts_nested_attributes_for :games, allow_destroy: true
end
game.rb
class Game < ApplicationRecord
belongs_to :tournament, optional: false
belongs_to :team_one, polymorphic: true
belongs_to :team_two, polymorphic: true
belongs_to :field, optional: true
end
schema.rb
ActiveRecord::Schema.define(version: 2019_12_24_011346) do
...
create_table "club_teams", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "fields", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "games", force: :cascade do |t|
t.bigint "tournament_id", null: false
t.string "team_one_type", null: false
t.bigint "team_one_id", null: false
t.string "team_two_type", null: false
t.bigint "team_two_id", null: false
t.bigint "field_id", null: false
t.date "date"
t.datetime "start_time"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["field_id"], name: "index_games_on_field_id"
t.index ["team_one_type", "team_one_id"], name: "index_games_on_team_one_type_and_team_one_id"
t.index ["team_two_type", "team_two_id"], name: "index_games_on_team_two_type_and_team_two_id"
t.index ["tournament_id"], name: "index_games_on_tournament_id"
end
create_table "high_school_teams", force: :cascade do |t|
t.string "school_name"
t.string "team_name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "tournaments", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "games", "fields"
add_foreign_key "games", "tournaments"
end
tournaments_controller.rb
class TournamentsController < ApplicationController
...
def create
@tournament = Tournament.new(tournament_params)
if @tournament.save
redirect_to @tournament
else
render 'new'
end
end
private
def tournament_params
params
.require(:tournament)
.permit(:name, games_attributes: [:id, :_destroy, :team_one_id, :team_two_id, :field_id, :date, :start_time])
end
end
请求参数在tournaments_controller#create
{
"authenticity_token"=>"iB4JefT9jRdiOFKok38OtjzMwd6Dv3hlHP/QZRtlFgMuVZfbn9PFD7Lebc1DuvfL6/IatDpS5CiubTci5MsCFg==",
"tournament"=>{
"name"=>"foo",
"games_attributes"=>{
"1577935885397"=>{
"team_one_id"=>"high-school-team-2",
"team_two_id"=>"club-team-2",
"date"=>"",
"start_time"=>"",
"_destroy"=>"false"
}
}
},
"commit"=>"Create Tournament"
}
tournament_params 在 tournaments_controller#create
<ActionController::Parameters {
"name"=>"foo",
"games_attributes"=><ActionController::Parameters {
"1577937916236"=><ActionController::Parameters {
"_destroy"=>"false",
"team_one_id"=>"high-school-team-2",
"team_two_id"=>"club-team-2",
"date"=>"",
"start_time"=>""
} permitted: true>
} permitted: true>
} permitted: true>
在我看来 tournament_params
与 accepts_nested_attributes documentation 在一对多下的预期匹配,所以我不明白为什么会出现错误。
Nested attributes for an associated collection can also be passed in
the form of a hash of hashes instead of an array of hashes:
Member.create(
name: 'joe',
posts_attributes: {
first: { title: 'Foo' },
second: { title: 'Bar' }
}
)
has the same effect as
Member.create(
name: 'joe',
posts_attributes: [
{ title: 'Foo' },
{ title: 'Bar' }
]
)
编辑:
tournaments/new.html.erb
<h1>Create a tournament</h1>
<%= render 'form' %>
<%= link_to 'Back', tournaments_path %>
tournaments/_form.html.erb
<%= form_with model: @tournament, class: 'tournament-form' do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<section class="games">
<%= f.fields_for :games do |game| %>
<%= render 'game_fields', f: game %>
<% end %>
<hr>
<p>
<%= link_to_add_association "Add game", f, :games,
data: {
association_insertion_node: '.games',
association_insertion_method: :prepend
}
%>
</p>
</section>
<p>
<%= f.submit %>
</p>
<% end %>
tournaments/_game_fields.html.erb
<section class="nested-fields">
<hr>
<p><strong>Game</strong></p>
<%= render "games/form_fields", f: f %>
<p><%= link_to_remove_association "Remove game", f %></p>
</section>
games/_form_fields.html.erb
<section>
<% if HighSchoolTeam.all.count + ClubTeam.all.count < 2 %>
<p>You neeed at least two teams to create a game. Create more high school and/or club teams first.</p>
<% else %>
<section class="game-form">
<p>
<%= f.label :team_one %><br>
<%= f.select :team_one_id, nil, {}, class: "team-one-dropdown" do %>
<optgroup label="High School Teams">
<% HighSchoolTeam.all.each do |high_school_team| %>
<option value="high-school-team-<%= high_school_team.id %>"><%= high_school_team.school_name %></option>
<% end %>
</optgroup>
<optgroup label="Club Teams">
<% ClubTeam.all.each do |club_team| %>
<option value="club-team-<%= club_team.id %>"><%= club_team.name %></option>
<% end %>
</optgroup>
<% end %>
</p>
<p>
<%= f.label :team_two %><br>
<%= f.select :team_two_id, nil, {}, class: "team-two-dropdown" do %>
<optgroup label="High School Teams">
<% HighSchoolTeam.all.each do |high_school_team| %>
<option value="high-school-team-<%= high_school_team.id %>"><%= high_school_team.school_name %></option>
<% end %>
</optgroup>
<optgroup label="Club Teams">
<% ClubTeam.all.each do |club_team| %>
<option value="club-team-<%= club_team.id %>"><%= club_team.name %></option>
<% end %>
</optgroup>
<% end %>
</p>
<p>
<%= f.label :field %><br>
<%= f.collection_select(:field_id, Field.all, :id, :name) %>
</p>
<p>
<%= f.label :date %><br>
<%= f.date_field :date %>
</p>
<p>
<%= f.label :start_time %><br>
<%= f.time_field :start_time %>
</p>
</section>
<% end %>
</section>
你似乎在拯救团队方面遇到了问题,而不是 Cocoon
gem 的问题。
因为您将 select 值自定义为 club-team-id
和 high-school-team-id
。我想你只需要把它改成这样:
<option value="HighSchoolTeam-<%= high_school_team.id %>"><%= high_school_team.school_name %></option>
和
<option value="ClubTeam-<%= club_team.id %>"><%= club_team.name %></option>
那么参数将是
{
"authenticity_token"=>"iB4JefT9jRdiOFKok38OtjzMwd6Dv3hlHP/QZRtlFgMuVZfbn9PFD7Lebc1DuvfL6/IatDpS5CiubTci5MsCFg==",
"tournament"=>{
"name"=>"foo",
"games_attributes"=>{
"1577935885397"=>{
"team_one_id"=>"HighSchoolTeam-2",
"team_two_id"=>"ClubTeam-2",
"date"=>"",
"start_time"=>"",
"_destroy"=>"false"
}
}
},
"commit"=>"Create Tournament"
}
那么您需要通过以下方式修改您的参数:
# Adding before_action on top of your controller
before_action :modify_params, only: [:create, :update]
private
# Not the cleanest way, but this is what I can think of right now.
def modify_params
params.dig(:tournament, :games_attributes).each do |game_id, game_attribute|
team_one_type = game_attribute[:team_one_id].split('-').first
team_one_id = game_attribute[:team_one_id].split('-').last
team_two_type = game_attribute[:team_two_id].split('-').first
team_two_id = game_attribute[:team_two_id].split('-').last
params[:tournament][:games_attributes][game_id] = game_attribute.merge(
team_one_type: team_one_type,
team_one_id: team_one_id,
team_two_type: team_two_type,
team_two_id: team_two_id
)
end
end
# And update this method to allow team_one_type and team_two_type
def tournament_params
params.require(:tournament)
.permit(:name, games_attributes: [:id, :_destroy, :team_one_id, :team_two_id, :team_one_type, :team_two_type, :field_id, :date, :start_time])
end
我正在使用 cocoon gem 构建用于创建锦标赛的表单。锦标赛 has_many
游戏。 Cocoon 让我可以动态地向表单中添加更多游戏。
当我调用 @tournament.save
时,它会生成以下错误:
Games team one must exist
Games team two must exist
tournament.rb
class Tournament < ApplicationRecord
has_many :games
accepts_nested_attributes_for :games, allow_destroy: true
end
game.rb
class Game < ApplicationRecord
belongs_to :tournament, optional: false
belongs_to :team_one, polymorphic: true
belongs_to :team_two, polymorphic: true
belongs_to :field, optional: true
end
schema.rb
ActiveRecord::Schema.define(version: 2019_12_24_011346) do
...
create_table "club_teams", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "fields", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "games", force: :cascade do |t|
t.bigint "tournament_id", null: false
t.string "team_one_type", null: false
t.bigint "team_one_id", null: false
t.string "team_two_type", null: false
t.bigint "team_two_id", null: false
t.bigint "field_id", null: false
t.date "date"
t.datetime "start_time"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["field_id"], name: "index_games_on_field_id"
t.index ["team_one_type", "team_one_id"], name: "index_games_on_team_one_type_and_team_one_id"
t.index ["team_two_type", "team_two_id"], name: "index_games_on_team_two_type_and_team_two_id"
t.index ["tournament_id"], name: "index_games_on_tournament_id"
end
create_table "high_school_teams", force: :cascade do |t|
t.string "school_name"
t.string "team_name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "tournaments", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "games", "fields"
add_foreign_key "games", "tournaments"
end
tournaments_controller.rb
class TournamentsController < ApplicationController
...
def create
@tournament = Tournament.new(tournament_params)
if @tournament.save
redirect_to @tournament
else
render 'new'
end
end
private
def tournament_params
params
.require(:tournament)
.permit(:name, games_attributes: [:id, :_destroy, :team_one_id, :team_two_id, :field_id, :date, :start_time])
end
end
请求参数在tournaments_controller#create
{
"authenticity_token"=>"iB4JefT9jRdiOFKok38OtjzMwd6Dv3hlHP/QZRtlFgMuVZfbn9PFD7Lebc1DuvfL6/IatDpS5CiubTci5MsCFg==",
"tournament"=>{
"name"=>"foo",
"games_attributes"=>{
"1577935885397"=>{
"team_one_id"=>"high-school-team-2",
"team_two_id"=>"club-team-2",
"date"=>"",
"start_time"=>"",
"_destroy"=>"false"
}
}
},
"commit"=>"Create Tournament"
}
tournament_params 在 tournaments_controller#create
<ActionController::Parameters {
"name"=>"foo",
"games_attributes"=><ActionController::Parameters {
"1577937916236"=><ActionController::Parameters {
"_destroy"=>"false",
"team_one_id"=>"high-school-team-2",
"team_two_id"=>"club-team-2",
"date"=>"",
"start_time"=>""
} permitted: true>
} permitted: true>
} permitted: true>
在我看来 tournament_params
与 accepts_nested_attributes documentation 在一对多下的预期匹配,所以我不明白为什么会出现错误。
Nested attributes for an associated collection can also be passed in the form of a hash of hashes instead of an array of hashes:
Member.create(
name: 'joe',
posts_attributes: {
first: { title: 'Foo' },
second: { title: 'Bar' }
}
)
has the same effect as
Member.create(
name: 'joe',
posts_attributes: [
{ title: 'Foo' },
{ title: 'Bar' }
]
)
编辑:
tournaments/new.html.erb
<h1>Create a tournament</h1>
<%= render 'form' %>
<%= link_to 'Back', tournaments_path %>
tournaments/_form.html.erb
<%= form_with model: @tournament, class: 'tournament-form' do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<section class="games">
<%= f.fields_for :games do |game| %>
<%= render 'game_fields', f: game %>
<% end %>
<hr>
<p>
<%= link_to_add_association "Add game", f, :games,
data: {
association_insertion_node: '.games',
association_insertion_method: :prepend
}
%>
</p>
</section>
<p>
<%= f.submit %>
</p>
<% end %>
tournaments/_game_fields.html.erb
<section class="nested-fields">
<hr>
<p><strong>Game</strong></p>
<%= render "games/form_fields", f: f %>
<p><%= link_to_remove_association "Remove game", f %></p>
</section>
games/_form_fields.html.erb
<section>
<% if HighSchoolTeam.all.count + ClubTeam.all.count < 2 %>
<p>You neeed at least two teams to create a game. Create more high school and/or club teams first.</p>
<% else %>
<section class="game-form">
<p>
<%= f.label :team_one %><br>
<%= f.select :team_one_id, nil, {}, class: "team-one-dropdown" do %>
<optgroup label="High School Teams">
<% HighSchoolTeam.all.each do |high_school_team| %>
<option value="high-school-team-<%= high_school_team.id %>"><%= high_school_team.school_name %></option>
<% end %>
</optgroup>
<optgroup label="Club Teams">
<% ClubTeam.all.each do |club_team| %>
<option value="club-team-<%= club_team.id %>"><%= club_team.name %></option>
<% end %>
</optgroup>
<% end %>
</p>
<p>
<%= f.label :team_two %><br>
<%= f.select :team_two_id, nil, {}, class: "team-two-dropdown" do %>
<optgroup label="High School Teams">
<% HighSchoolTeam.all.each do |high_school_team| %>
<option value="high-school-team-<%= high_school_team.id %>"><%= high_school_team.school_name %></option>
<% end %>
</optgroup>
<optgroup label="Club Teams">
<% ClubTeam.all.each do |club_team| %>
<option value="club-team-<%= club_team.id %>"><%= club_team.name %></option>
<% end %>
</optgroup>
<% end %>
</p>
<p>
<%= f.label :field %><br>
<%= f.collection_select(:field_id, Field.all, :id, :name) %>
</p>
<p>
<%= f.label :date %><br>
<%= f.date_field :date %>
</p>
<p>
<%= f.label :start_time %><br>
<%= f.time_field :start_time %>
</p>
</section>
<% end %>
</section>
你似乎在拯救团队方面遇到了问题,而不是 Cocoon
gem 的问题。
因为您将 select 值自定义为 club-team-id
和 high-school-team-id
。我想你只需要把它改成这样:
<option value="HighSchoolTeam-<%= high_school_team.id %>"><%= high_school_team.school_name %></option>
和
<option value="ClubTeam-<%= club_team.id %>"><%= club_team.name %></option>
那么参数将是
{
"authenticity_token"=>"iB4JefT9jRdiOFKok38OtjzMwd6Dv3hlHP/QZRtlFgMuVZfbn9PFD7Lebc1DuvfL6/IatDpS5CiubTci5MsCFg==",
"tournament"=>{
"name"=>"foo",
"games_attributes"=>{
"1577935885397"=>{
"team_one_id"=>"HighSchoolTeam-2",
"team_two_id"=>"ClubTeam-2",
"date"=>"",
"start_time"=>"",
"_destroy"=>"false"
}
}
},
"commit"=>"Create Tournament"
}
那么您需要通过以下方式修改您的参数:
# Adding before_action on top of your controller
before_action :modify_params, only: [:create, :update]
private
# Not the cleanest way, but this is what I can think of right now.
def modify_params
params.dig(:tournament, :games_attributes).each do |game_id, game_attribute|
team_one_type = game_attribute[:team_one_id].split('-').first
team_one_id = game_attribute[:team_one_id].split('-').last
team_two_type = game_attribute[:team_two_id].split('-').first
team_two_id = game_attribute[:team_two_id].split('-').last
params[:tournament][:games_attributes][game_id] = game_attribute.merge(
team_one_type: team_one_type,
team_one_id: team_one_id,
team_two_type: team_two_type,
team_two_id: team_two_id
)
end
end
# And update this method to allow team_one_type and team_two_type
def tournament_params
params.require(:tournament)
.permit(:name, games_attributes: [:id, :_destroy, :team_one_id, :team_two_id, :team_one_type, :team_two_type, :field_id, :date, :start_time])
end