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_paramsaccepts_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-idhigh-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