从表单输入与关联模型 ID 的数组建立关联
Building associations from a form input with array of associated model id's
我有模型游戏、玩家和国家。我正在为 Player
开发一个带有嵌套字段的表单,它应该创建具有相关国家/地区的播放器。
country_ids
的数组作为值数组通过 nested_player.hidden_field :country_ids
发送。
游戏:
class Game < ApplicationRecord
has_and_belongs_to_many :players
accepts_nested_attributes_for :players
end
玩家:
class Player < ApplicationRecord
has_and_belongs_to_many :games
has_many :countries
end
国家:
class Country < ApplicationRecord
belongs_to :player, optional: true
end
游戏控制器:
def game_params
params.require(:game).permit(:metadata, players_attributes: [:name, :color, country_ids: []])
end
形式:
<%= simple_form_for @game do |f| %>
<%= f.fields_for :players do |player| %>
<%= player.input :name %>
<%= player.input :color, as: :color %>
<%= player.hidden_field :country_ids, value: ["226"] %>
<% end %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
问题:
控制器正在按预期接收 country_ids
。游戏和嵌套 Player
已保存,但未建立玩家-国家/地区关联。
参数:
{"game"=>{"players_attributes"=>{"0"=>{"name"=>"foo", "color"=>"#000000", "country_ids"=>"226"}}},
"commit"=>"Submit"}
您目前的设置方式会在您每次创建新游戏时重新分配 Country#player_id
新玩家,最后一个玩家 ID 将在 Country#player_id
中;现在是一个国家 -> 一个玩家。
要修复它,请在国家和玩家之间添加另一个连接 table。
# db/migrate/20220419040615_create_pink_floyd90_game.rb
class CreatePinkFloyd90Game < ActiveRecord::Migration[7.0]
def change
create_table :countries do |t|
t.string :name
end
create_table :games do |t|
t.string :name
end
create_table :players do |t|
t.string :name
end
create_join_table :countries, :players # fixed
create_join_table :games, :players
end
end
# app/models/*.rb
class Country < ApplicationRecord
has_and_belongs_to_many :players
end
class Player < ApplicationRecord
has_and_belongs_to_many :games
has_and_belongs_to_many :countries
end
class Game < ApplicationRecord
has_and_belongs_to_many :players
accepts_nested_attributes_for :players
end
在设置表格之前,如果关联不明显,最好测试一下:
>> Country.create!([{name: 'Country 1'}, {name: 'Country 2'}])
>> Game.create!(name: 'Game 1', players_attributes: {one: {name: 'Player 1', country_ids: [1,2]}})
# NOTE: notice the actual records that are created, and make sure this is the intention
# Load the referenced by ids countries (for validation, I think)
Country Load (0.7ms) SELECT "countries".* FROM "countries" WHERE "countries"."id" IN (, ) [["id", 1], ["id", 2]]
TRANSACTION (0.3ms) BEGIN
# Create a game
Game Create (0.7ms) INSERT INTO "games" ("name") VALUES () RETURNING "id" [["name", "Game 1"]]
# Create a player
Player Create (0.7ms) INSERT INTO "players" ("name") VALUES () RETURNING "id" [["name", "Player 1"]]
# Associate 'Country 1' with 'Player 1'
Player::HABTM_Countries Create (0.5ms) INSERT INTO "countries_players" ("country_id", "player_id") VALUES (, ) [["country_id", 1], ["player_id", 1]]
# Associate 'Country 2' with 'Player 1'
Player::HABTM_Countries Create (0.3ms) INSERT INTO "countries_players" ("country_id", "player_id") VALUES (, ) [["country_id", 2], ["player_id", 1]]
# Associate 'Game 1' with 'Player 1'
Game::HABTM_Players Create (0.5ms) INSERT INTO "games_players" ("game_id", "player_id") VALUES (, ) [["game_id", 1], ["player_id", 1]]
TRANSACTION (2.8ms) COMMIT
=> #<Game:0x00007f3ca4a82540 id: 1, name: "Game 1">
>> Game.first.players.pluck(:name)
=> ["Player 1"]
>> Player.first.countries.pluck(:name)
=> ["Country 1", "Country 2"]
现在我知道这是可行的,任何意外的事情都会出现在控制器或表单中。第二个问题在哪,原因player-country没有关联。
{
"game"=>{
"players_attributes"=>{
"0"=>{
"name"=>"foo",
"color"=>"#000000",
"country_ids"=>"226" # doesn't look like an array
}
}
},
"commit"=>"Submit"
}
因为country_ids
不是数组,也不是rails识别为数组的散列{ "0"=>{}, "1"=>{} }
允许的参数不允许通过。
Game.create(game_params) # <= never receives `country_ids`
易于签入rails控制台
https://api.rubyonrails.org/classes/ActionController/Parameters.html
# this is a regular attribute, not an array
>> params = {"country_ids"=>"1"}
>> ActionController::Parameters.new(params).permit(country_ids: []).to_h
=> {} # not allowed
# how about a nested hash
>> params = {"country_ids"=>{"0"=>{"id"=>"1"}}}
>> ActionController::Parameters.new(params).permit(country_ids: [:id]).to_h
=> {"country_ids"=>{"0"=>{"id"=>"1"}}} # allowed, but not usable without modifications
# how about an array
>> params = {"country_ids"=>["1","2"]}
>> ActionController::Parameters.new(params).permit(country_ids: []).to_h
=> {"country_ids"=>["1", "2"]} # TADA!
如何使表单提交数组。
提交实际数组有点麻烦。诀窍是使输入 name
属性以 []
结尾。对于 country_ids
,输入应如下所示
<input value="1" type="text" name="game[players_attributes][0][country_ids][]">
<input value="2" type="text" name="game[players_attributes][0][country_ids][]">
# this will submit these parameters
# {"game"=>{"players_attributes"=>{"0"=>{"country_ids"=>["1", "2"]}}}
表单构建者似乎不喜欢这种设置,所以我们必须做一些恶作剧,尤其是对于这种嵌套设置:
form_for
<% game.players.build %>
<%= form_with model: game do |f| %>
<%= f.fields_for :players do |ff| %>
<%# using plain tag helper %>
<%# NOTE: `ff.object_name` returns "game[players_attributes][0]" %>
<%= text_field_tag "#{ff.object_name}[country_ids][]", 1 %> <%# <input type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_" value="1"> %>
<%= text_field_tag "#{ff.object_name}[country_ids][]", 2 %> <%# <input type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_" value="2"> %>
<%# using `fields_for` helper %>
<%= ff.fields_for :country_ids do |fff| %>
<%# NOTE: empty string '' gives us [] %>
<%= fff.text_field '', value: 1 %> <%# <input value="1" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<%= fff.text_field '', value: 2 %> <%# <input value="2" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
simple_form
一样
<% game.players.build %>
<%= simple_form_for game do |f| %>
<%= f.simple_fields_for :players do |ff| %>
<%= ff.simple_fields_for :country_ids do |fff| %>
<%= fff.input '', input_html: { value: 1 } %> <%# <input value="1" class="string required" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<%= fff.input '', input_html: { value: 2 } %> <%# <input value="2" class="string required" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<% end %>
<% end %>
<% end %>
或者忘记所有这些,因为它很复杂,并且将 country_ids
作为纯字符串并在控制器中拆分它
<%= simple_form_for game do |f| %>
<%= f.simple_fields_for :players do |ff| %>
<%= ff.input :country_ids, input_html: { value: [1,2] } %> <%# <input value="1 2" class="string optional" type="text" name="game[players_attributes][0][country_ids]" id="game_players_attributes_0_country_ids"> %>
<% end %>
<%= f.submit %>
<% end %>
def game_params
# modify only once
@game_params ||= modify_params(
params.require(:game).permit(players_attributes: [:name, :country_ids])
)
end
def modify_params permitted
# NOTE: take "1 2" and split into ["1", "2"]
permitted[:players_attributes].each_value{|p| p[:country_ids] = p[:country_ids].split }
permitted
end
def create
Game.create(game_params)
end
希望这不会太混乱。
我有模型游戏、玩家和国家。我正在为 Player
开发一个带有嵌套字段的表单,它应该创建具有相关国家/地区的播放器。
country_ids
的数组作为值数组通过 nested_player.hidden_field :country_ids
发送。
游戏:
class Game < ApplicationRecord
has_and_belongs_to_many :players
accepts_nested_attributes_for :players
end
玩家:
class Player < ApplicationRecord
has_and_belongs_to_many :games
has_many :countries
end
国家:
class Country < ApplicationRecord
belongs_to :player, optional: true
end
游戏控制器:
def game_params
params.require(:game).permit(:metadata, players_attributes: [:name, :color, country_ids: []])
end
形式:
<%= simple_form_for @game do |f| %>
<%= f.fields_for :players do |player| %>
<%= player.input :name %>
<%= player.input :color, as: :color %>
<%= player.hidden_field :country_ids, value: ["226"] %>
<% end %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
问题:
控制器正在按预期接收 country_ids
。游戏和嵌套 Player
已保存,但未建立玩家-国家/地区关联。
参数:
{"game"=>{"players_attributes"=>{"0"=>{"name"=>"foo", "color"=>"#000000", "country_ids"=>"226"}}},
"commit"=>"Submit"}
您目前的设置方式会在您每次创建新游戏时重新分配 Country#player_id
新玩家,最后一个玩家 ID 将在 Country#player_id
中;现在是一个国家 -> 一个玩家。
要修复它,请在国家和玩家之间添加另一个连接 table。
# db/migrate/20220419040615_create_pink_floyd90_game.rb
class CreatePinkFloyd90Game < ActiveRecord::Migration[7.0]
def change
create_table :countries do |t|
t.string :name
end
create_table :games do |t|
t.string :name
end
create_table :players do |t|
t.string :name
end
create_join_table :countries, :players # fixed
create_join_table :games, :players
end
end
# app/models/*.rb
class Country < ApplicationRecord
has_and_belongs_to_many :players
end
class Player < ApplicationRecord
has_and_belongs_to_many :games
has_and_belongs_to_many :countries
end
class Game < ApplicationRecord
has_and_belongs_to_many :players
accepts_nested_attributes_for :players
end
在设置表格之前,如果关联不明显,最好测试一下:
>> Country.create!([{name: 'Country 1'}, {name: 'Country 2'}])
>> Game.create!(name: 'Game 1', players_attributes: {one: {name: 'Player 1', country_ids: [1,2]}})
# NOTE: notice the actual records that are created, and make sure this is the intention
# Load the referenced by ids countries (for validation, I think)
Country Load (0.7ms) SELECT "countries".* FROM "countries" WHERE "countries"."id" IN (, ) [["id", 1], ["id", 2]]
TRANSACTION (0.3ms) BEGIN
# Create a game
Game Create (0.7ms) INSERT INTO "games" ("name") VALUES () RETURNING "id" [["name", "Game 1"]]
# Create a player
Player Create (0.7ms) INSERT INTO "players" ("name") VALUES () RETURNING "id" [["name", "Player 1"]]
# Associate 'Country 1' with 'Player 1'
Player::HABTM_Countries Create (0.5ms) INSERT INTO "countries_players" ("country_id", "player_id") VALUES (, ) [["country_id", 1], ["player_id", 1]]
# Associate 'Country 2' with 'Player 1'
Player::HABTM_Countries Create (0.3ms) INSERT INTO "countries_players" ("country_id", "player_id") VALUES (, ) [["country_id", 2], ["player_id", 1]]
# Associate 'Game 1' with 'Player 1'
Game::HABTM_Players Create (0.5ms) INSERT INTO "games_players" ("game_id", "player_id") VALUES (, ) [["game_id", 1], ["player_id", 1]]
TRANSACTION (2.8ms) COMMIT
=> #<Game:0x00007f3ca4a82540 id: 1, name: "Game 1">
>> Game.first.players.pluck(:name)
=> ["Player 1"]
>> Player.first.countries.pluck(:name)
=> ["Country 1", "Country 2"]
现在我知道这是可行的,任何意外的事情都会出现在控制器或表单中。第二个问题在哪,原因player-country没有关联。
{
"game"=>{
"players_attributes"=>{
"0"=>{
"name"=>"foo",
"color"=>"#000000",
"country_ids"=>"226" # doesn't look like an array
}
}
},
"commit"=>"Submit"
}
因为country_ids
不是数组,也不是rails识别为数组的散列{ "0"=>{}, "1"=>{} }
允许的参数不允许通过。
Game.create(game_params) # <= never receives `country_ids`
易于签入rails控制台
https://api.rubyonrails.org/classes/ActionController/Parameters.html
# this is a regular attribute, not an array
>> params = {"country_ids"=>"1"}
>> ActionController::Parameters.new(params).permit(country_ids: []).to_h
=> {} # not allowed
# how about a nested hash
>> params = {"country_ids"=>{"0"=>{"id"=>"1"}}}
>> ActionController::Parameters.new(params).permit(country_ids: [:id]).to_h
=> {"country_ids"=>{"0"=>{"id"=>"1"}}} # allowed, but not usable without modifications
# how about an array
>> params = {"country_ids"=>["1","2"]}
>> ActionController::Parameters.new(params).permit(country_ids: []).to_h
=> {"country_ids"=>["1", "2"]} # TADA!
如何使表单提交数组。
提交实际数组有点麻烦。诀窍是使输入 name
属性以 []
结尾。对于 country_ids
,输入应如下所示
<input value="1" type="text" name="game[players_attributes][0][country_ids][]">
<input value="2" type="text" name="game[players_attributes][0][country_ids][]">
# this will submit these parameters
# {"game"=>{"players_attributes"=>{"0"=>{"country_ids"=>["1", "2"]}}}
表单构建者似乎不喜欢这种设置,所以我们必须做一些恶作剧,尤其是对于这种嵌套设置:
form_for
<% game.players.build %>
<%= form_with model: game do |f| %>
<%= f.fields_for :players do |ff| %>
<%# using plain tag helper %>
<%# NOTE: `ff.object_name` returns "game[players_attributes][0]" %>
<%= text_field_tag "#{ff.object_name}[country_ids][]", 1 %> <%# <input type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_" value="1"> %>
<%= text_field_tag "#{ff.object_name}[country_ids][]", 2 %> <%# <input type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_" value="2"> %>
<%# using `fields_for` helper %>
<%= ff.fields_for :country_ids do |fff| %>
<%# NOTE: empty string '' gives us [] %>
<%= fff.text_field '', value: 1 %> <%# <input value="1" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<%= fff.text_field '', value: 2 %> <%# <input value="2" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
simple_form
一样
<% game.players.build %>
<%= simple_form_for game do |f| %>
<%= f.simple_fields_for :players do |ff| %>
<%= ff.simple_fields_for :country_ids do |fff| %>
<%= fff.input '', input_html: { value: 1 } %> <%# <input value="1" class="string required" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<%= fff.input '', input_html: { value: 2 } %> <%# <input value="2" class="string required" type="text" name="game[players_attributes][0][country_ids][]" id="game_players_attributes_0_country_ids_"> %>
<% end %>
<% end %>
<% end %>
或者忘记所有这些,因为它很复杂,并且将 country_ids
作为纯字符串并在控制器中拆分它
<%= simple_form_for game do |f| %>
<%= f.simple_fields_for :players do |ff| %>
<%= ff.input :country_ids, input_html: { value: [1,2] } %> <%# <input value="1 2" class="string optional" type="text" name="game[players_attributes][0][country_ids]" id="game_players_attributes_0_country_ids"> %>
<% end %>
<%= f.submit %>
<% end %>
def game_params
# modify only once
@game_params ||= modify_params(
params.require(:game).permit(players_attributes: [:name, :country_ids])
)
end
def modify_params permitted
# NOTE: take "1 2" and split into ["1", "2"]
permitted[:players_attributes].each_value{|p| p[:country_ids] = p[:country_ids].split }
permitted
end
def create
Game.create(game_params)
end
希望这不会太混乱。