Search code examples
ruby-on-railsnested-attributes

accept_nested_attributes_for creates one extra nil record on Rails


I am using Rails as a backend and React as my front-end. On the React side, I am using fetch to do POST request to my model named schedule. I am also adding a child attributes for worker model.

Here are some code snippets that I have. I am using has_many :through relationship in rails.

My Rails models and controller:

//schedule.rb
  has_many :workers, through: :rosters, dependent: :destroy
  has_many :rosters, inverse_of: :schedule

//worker.rb
  has_many :schedules, through: :rosters
  has_many :rosters, inverse_of: :worker

//roster.rb
  belongs_to :schedule
  belongs_to :worker

//schedules_controller.rb
def create
    @schedule = Schedule.new(schedule_params)
    @workers = @schedule.rosters.build.build_worker
    if @schedule.save
      render json: @schedule
    else
      render json: @schedule, status: :unprocessable_entity
    end
  end 
  ...
  def schedule_params
    params.permit(:date, :user_id, :workers_attributes => [:id, :name, :phone])
  end

On React side:

//inside Client.js
function postSchedule(date, cb) {
  return fetch(`api/schedules`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      date: date,
      user_id: 1,
      workers_attributes: [{name: "Iggy Test", phone: "123-456-7890"}, {name: "Iggy Test 2", phone: "987-654-3210"}]
    })
  }).then((response) => response.json())
    .then(cb);
};



//inside main app:
  postSchedule(){
      Client.postSchedule(this.state.date, (schedule) => {
        this.setState({schedules: this.state.schedules.concat([schedule])})
      })
  };

The problem that I have is, when I submit a new schedule, I expect to see a new schedule with two workers: "Iggy Test" and "Iggy Test 2". However, when I looked inside Rails, it is creating 3 workers: "Iggy Test", "Iggy Test 2", and nil.

Here is what is happening when I submit the request:

Started POST "/api/schedules" for 127.0.0.1 at 2017-05-24 09:55:16 -0700
Processing by SchedulesController#create as */*
  Parameters: {"date"=>"2017-05-27T02:00:00.000Z", "user_id"=>1, "workers_attributes"=>[{"name"=>"Iggy Test", "phone"=>"
123-456-7890"}, {"name"=>"Iggy Test 2", "phone"=>"987-654-3210"}], "schedule"=>{"date"=>"2017-05-27T02:00:00.000Z", "use
r_id"=>1}}
Unpermitted parameter: schedule
   (0.1ms)  begin transaction
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.4ms)  INSERT INTO "schedules" ("date", "created_at", "updated_at", "user_id") VALUES (?, ?, ?, ?)  [["date", 20
17-05-27 02:00:00 UTC], ["created_at", 2017-05-24 16:55:16 UTC], ["updated_at", 2017-05-24 16:55:16 UTC], ["user_id", 1]
]
  SQL (0.2ms)  INSERT INTO "workers" ("name", "phone", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Iggy
Test"], ["phone", "123-456-7890"], ["created_at", 2017-05-24 16:55:16 UTC], ["updated_at", 2017-05-24 16:55:16 UTC]]

  SQL (0.2ms)  INSERT INTO "rosters" ("worker_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["worker_id", 64], ["c
reated_at", 2017-05-24 16:55:16 UTC], ["updated_at", 2017-05-24 16:55:16 UTC]]
  SQL (0.1ms)  INSERT INTO "workers" ("name", "phone", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Iggy
Test 2"], ["phone", "987-654-3210"], ["created_at", 2017-05-24 16:55:16 UTC], ["updated_at", 2017-05-24 16:55:16 UTC]]
  SQL (0.1ms)  INSERT INTO "rosters" ("worker_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["worker_id", 65], ["c
reated_at", 2017-05-24 16:55:16 UTC], ["updated_at", 2017-05-24 16:55:16 UTC]]
  SQL (0.2ms)  INSERT INTO "workers" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", 2017-05-24 16:55:16 UTC
], ["updated_at", 2017-05-24 16:55:16 UTC]]
  SQL (0.2ms)  INSERT INTO "rosters" ("schedule_id", "worker_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["sc
hedule_id", 57], ["worker_id", 66], ["created_at", 2017-05-24 16:55:16 UTC], ["updated_at", 2017-05-24 16:55:16 UTC]]
  SQL (0.7ms)  UPDATE "rosters" SET "schedule_id" = ?, "updated_at" = ? WHERE "rosters"."id" = ?  [["schedule_id", 57],
["updated_at", 2017-05-24 16:55:16 UTC], ["id", 60]]
  SQL (0.1ms)  UPDATE "rosters" SET "schedule_id" = ?, "updated_at" = ? WHERE "rosters"."id" = ?  [["schedule_id", 57],
["updated_at", 2017-05-24 16:55:16 UTC], ["id", 61]]
   (2.6ms)  commit transaction
Completed 200 OK in 68ms (Views: 0.8ms | ActiveRecord: 5.6ms)

The log created a schedule, then a worker (Iggy Test), then a roster (for that schedule and Iggy Test), then another worker (Iggy Test 2), then another roster (for Iggy Test 2 and that schedule) - instead of stopping, it created another worker (nil) and a roster for that nil worker.

Why is it behaving such? How can I fix it to create only the specified workers?

As a side note - if you noticed, the log says unpermitted parameter: schedule. That message disappears when I add require(:schedule) inside my schedule_params, but it would instead create only one nil worker.


Solution

  • accepts_nested_attributes_for is not creating an extra record. You are.

    def create
      @schedule = Schedule.new(schedule_params)
      # This adds a worker with no attributes
      @workers = @schedule.rosters.build.build_worker
      if @schedule.save
        render json: @schedule
      else
        render json: @schedule, status: :unprocessable_entity
      end
    end
    

    "Unpermitted parameter: schedule" is just a warning that there was a param is in the params hash that was not whitelisted by .permit. It is logged since it could be a malicious attempt to fish for mass assignment vulnerabilities.

    .require takes a single key from the params hash and raises an error if it not present and is not what you want when you have a flat params hash.

    Instead you should look into why the params sent by react include both the unwrapped params and I don't get why you are sending both the unwrapped params and a "schedule"=>{"date"=>"2017-05-27T02:00:00.000Z", "user_id"=>1} hash. I don't really know React but I'm guessing it has something to do with this.state.schedules.concat([schedule])