Creating Rails 4 Form with Join Table Metadata

2019-07-24 20:36发布

问题:

Very new Rails 4 developer here. I've got a form where a user is creating Exercises. Exercises can have many Equipment, and Equipment can be optional( think push-up stands for doing push-ups ). I store this "optional" field on the join table exercise_equipment.

I cannot get the parameters to actually send through the values of the collection element that I pick. See below for the model, view, controller, and parameters.

Here are the attributes/relationships of my models:

# id        :integer
# name      :string
# is_public :boolean
Exercise
    has_many :exercise_equipment
    has_many :equipment, :through => :exercise_equipment
    accepts_nested_attributes_for :exercise_equipment


# id           :integer
# exercise_id  :integer
# equipment_id :integer
# optional     :boolean
ExerciseEquipment
    belongs_to :exercise
    belongs_to :equipment
    accepts_nested_attributes_for :equipment


# id   :integer
# name :string
Equipment
    has_many :exercise_equipment
    has_many :exercises, :through => :exercise_equipment

Here are some (maybe) relevant controller methods:

def new
  @exercise = Exercise.new
  @exercise.exercise_equipment.build
end

def create
  @exercise = Exercise.new( exercise_params )
  if @exercise.save
    redirect_to @exercises
  else
    render 'new'
  end
end

def edit
  @exercise = Exercise.find( params[:id] )
end

def update
  @exercise = Exercise.find( params[:id] )
  if @exercise.update_attributes( exercise_params )
    redirect_to @exercises
  else
    render 'edit'
  end
end

def exercise_params
  params.require( :exercise ).permit(
    :name,
    :is_public,
    exercise_equipment_attributes: [
      :id,
      :optional,
      equipment_attributes: [
        :id,
        :name
      ],
    ]
  )
end

This is my shot at creating a view to do what I want:

exercises/new.html.erb

<%= form_for @exercise do |f| %>
  <%= render 'form', f: f %>
  <%= f.submit "New Exercise" %>
<% end %>

exercises/_form.html.erb

<%= f.label :name %><br />
<%= f.text_field :name %>

<%= f.check_box :is_public %> Public

<%= f.fields_for( :exercise_equipment ) do |eef|
  <%= eef.fields_for( :equipment ) do |ef|
    ef.collection_select :id, Equipment.all, :id, :name %>
  <% end %>
  <%= eef.check_box :is_optional %> Optional
<% end %>

When I put all of this together and submit an update to an already-existing exercise, the values all go through the params hash, but aren't changed to the new values I've selected...

Parameters: {
  "utf8"=>"[checkbox]",
  "authenticity_token"=>"[token]",
  "exercise"=>{
    "name"=>"Test", 
    "is_public"=>"1", 
    "exercise_equipment_attributes"=>{
      "0"=>{
        "equipment_attributes"=>{
          "id"=>"1"
        }, 
        "optional"=>"1", 
        "id"=>"2" 
      } 
    } 
  },
  "commit"=>"Save Exercise",
  "id"=>"1" 
}

If you can help me out, I'd be super appreciative. Just let me know if you need any more information and I can provide it.

EDIT

Here is the state of the database before updating:

postgres@=>db=# select id, name, is_public from exercises;
 id | name | is_public 
----+------+-----------
  2 | Test | t
(1 row)

Time: 61.279 ms
postgres@=>db=# select id, exercise_id, equipment_id, optional from exercise_equipment;
 id | exercise_id | equipment_id | optional 
----+-------------+--------------+----------
  2 |           2 |            1 | t
(1 row)

Time: 58.819 ms
postgres@=>db=# select id, name from equipment where id = 1;
 id |    name     
----+-------------
  1 | Freeweights
(1 row)

I then go to the update route for that exercise, select a different equipment from the collection, and submit the form. I get the following Rails Console results:

Started PATCH "/exercises/system-test" for 127.0.0.1 at 2014-08-12 23:48:18 -0400
Processing by ExercisesController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"PsbbUPSCiIew2Fd22Swn+K4PmLjwNDCrDdwXf9YBcm8=", "exercise"=>{"name"=>"Test", "is_public"=>"1", "exercise_equipment_attributes"=>{"0"=>{"equipment_attributes"=>{"id"=>"1"}, "optional"=>"1", "id"=>"2"}}}, "commit"=>"Save Exercise", "id"=>"system-test"}
  Exercise Load (60.5ms)  SELECT  "exercises".* FROM "exercises"  WHERE "exercises"."slug" = 'system-test'  ORDER BY "exercises"."id" ASC LIMIT 1
   (57.3ms)  BEGIN
  ExerciseEquipment Load (76.2ms)  SELECT "exercise_equipment".* FROM "exercise_equipment"  WHERE "exercise_equipment"."exercise_id" = $1 AND "exercise_equipment"."id" IN (2)  [["exercise_id", 2]]
  Equipment Load (59.1ms)  SELECT  "equipment".* FROM "equipment"  WHERE "equipment"."id" = $1 LIMIT 1  [["id", 1]]
  User Load (60.3ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = $1 LIMIT 1  [["id", 10]]
  Exercise Exists (60.5ms)  SELECT  1 AS one FROM "exercises"  WHERE ("exercises"."name" = 'Test' AND "exercises"."id" != 2 AND "exercises"."user_id" = 10) LIMIT 1
   (64.8ms)  COMMIT
Redirected to http://localhost:3000/exercises/system-test
Completed 302 Found in 590ms (ActiveRecord: 580.0ms)


Started GET "/exercises/system-test" for 127.0.0.1 at 2014-08-12 23:48:19 -0400
Processing by ExercisesController#show as HTML
  Parameters: {"id"=>"system-test"}
  Exercise Load (64.1ms)  SELECT  "exercises".* FROM "exercises"  WHERE "exercises"."slug" = 'system-test'  ORDER BY "exercises"."id" ASC LIMIT 1
  Equipment Load (58.7ms)  SELECT "equipment".* FROM "equipment" INNER JOIN "exercise_equipment" ON "equipment"."id" = "exercise_equipment"."equipment_id" WHERE "exercise_equipment"."exercise_id" = $1  [["exercise_id", 2]]
  Rendered exercises/show.html.erb within layouts/application (122.7ms)
  User Load (60.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = 10  ORDER BY "users"."id" ASC LIMIT 1
  Rendered shared/_header.html.erb (61.9ms)
  Rendered shared/_alerts.html.erb (0.1ms)
Completed 200 OK in 264ms (Views: 21.3ms | ActiveRecord: 240.8ms)

回答1:

Firstly, you need to make sure you define your associations correctly.

Any has_many association should be defined with a plural name -

#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
    has_many :exercise_equipments
    has_many :equipments, :through => :exercise_equipments

    accepts_nested_attributes_for :exercise_equipments
end

#app/models/exercise_equipment.rb
Class ExerciseEquipment < ActiveRecord::Base
   belongs_to :exercise
   belongs_to :equipment
end

#app/models/equipment.rb
Class Equipment < ActiveRecord::Base
   has_many :exercise_equipments
   has_many :exercises, through: :exercise_equipments
end

If you've already got it working, and are happy with what you've got, then I'd recommend keeping your current setup. However, you may wish to adopt the above for convention's sake

Edit I see from the deleted answer that Beartech investigated this, and turns out Rails treats Equipment / Equipments as the same. Will be worth ignoring the above, but I'll leave it for future reference


Params

I cannot get the parameters to actually send through the values of the collection element that I pick. See below for the model, view, controller, and parameters.

I think I get what you mean - you're looking to update the record, but it does not send through the updated parameters to your controller, hence preventing it from being updated.

Although I can't see any glaring problems, I would recommend the issue is that you're trying to populate the exercise_id of an Exercise object. You need to define it for the exercise_equipment object:

<%= f.fields_for :exercise_equipment do |eef| %>
   <%= eef.collection_select :equipment_id, Equipment.all, :id, :name %>
   <%= eef.check_box :is_optional %>
<% end %>

This will populate your exercise_equipment table as described here:

Time: 61.279 ms
postgres@=>db=# select id, exercise_id, equipment_id, optional from exercise_equipment;
 id | exercise_id | equipment_id | optional 
----+-------------+--------------+----------
  2 |           2 |            1 | t
(1 row)

Currently, you're populating the Equipment model with equipment_id - which won't work. Populating the model in that way will server to create a new record, not update the ones already created


Extra Field

I want to have a link to add an additional equipment field when it is clicked, similar to how Ryan Bates did it in this RailsCast, but the helper method he writes( see "Show Notes" tab if you're not subscribed to see the source ) seems to become substantially more complex when dealing with the nested views shown in my code below. Any help in dealing with this?

This a trickier mountain to overcome

Ryan uses quite an outdated method in this process (to pre-populate the link and then just let JS append the field). The "right" way is to build a new object & append the fields_for from ajax. Sounds tough? That's because it is :)

Here's how you do it:

#config/routes.rb
resources :exercises do
   collection do
       get :ajax_update #-> domain.com/exercises/ajax_update
   end
end

#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
   def self.build
       exercise = self.new
       exercise.exercise_equipment.build
   end
end

#app/controllers/exercises_controller.rb
Class ExercisesController < ApplicationController
   def new
       @exercise = Exercise.build
   end

   def ajax_update
      @exercise = Exercise.build
      render "add_exercise", layout: false #> renders form with fields_for
   end
end

#app/views/exercises/add_exercise.html.erb
<%= form_for @exercise do |f| %>
   <%= render partial: "fields_for", locals: { form: f } %>
<% end %>

#app/views/exercises/_fields_for.html.erb
<%= f.fields_for :exercise_equipment, child_index: Time.now.to_i do |eef| %>
    <%= eef.collection_select :equipment_id, Equipment.all, :id, :name  %>
    <%= eef.check_box :is_optional %>
<% end %>

#app/views/exercises/edit.html.erb
<%= form_for @exercise do |f| %>
    <%= render partial: "fields_for", locals: { form: f } %>
    <%= link_to "Add Field", "#", id: "add_field" %>
<% end %>

#app/assets/javascripts/application.js
$(document).on("click", "#add_field", function() {
    $.ajax({
       url: "exercises/ajax_update",
       success: function(data) {
          el_to_add = $(data).html()
          $('#your_id').append(el_to_add)
       }
    });
 });