John Elent
John Elent

Reputation: 103

Nested attributes with has_many through association creating object twice

I have Rails API application with many to many relationship between users and projects though project_memberships table.

Models:

class User < ActiveRecord::Base
  has_many :project_memberships, dependent: :destroy
  has_many :projects, -> { uniq }, through: :project_memberships

  accepts_nested_attributes_for :project_memberships, allow_destroy: true
end

class Project < ActiveRecord::Base
  has_many :project_memberships, dependent: :destroy
  has_many :users, -> { uniq }, through: :project_memberships
end

class ProjectMembership < ActiveRecord::Base
  belongs_to :user
  belongs_to :project

  validates :user, presence: true
  validates :project, presence: true
end

Controller:

class UsersController < ApplicationController
  expose(:user, attributes: :user_params)

  respond_to :json

  # removed unrelated actions

  def update
    user.update user_params
    respond_with user
  end

  private

  def user_params
    params.require(:user).permit(
      :first_name, :last_name,
      project_memberships_attributes: 
        [:id, :project_id, :membership_starts_at, :_destroy]
    )
  end
end

The problem is that when I send a PUT request to http://localhost:3000/users/1 with the following json:

{"user":{"project_memberships_attributes":[{"project_id": 1}]}

user.update user_params creates 2 ProjectMembership records with the same user_id and project_id.

  SQL (0.3ms)  INSERT INTO "project_memberships" ("project_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["project_id", 1], ["user_id", 1], ["created_at", "2016-03-18 18:00:07.670012"], ["updated_at", "2016-03-18 18:00:07.670012"]]
  SQL (0.2ms)  INSERT INTO "project_memberships" ("project_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["project_id", 1], ["user_id", 1], ["created_at", "2016-03-18 18:00:07.671644"], ["updated_at", "2016-03-18 18:00:07.671644"]]
  (1.0ms)  COMMIT

Btw destroying and updating already existing records by specifying id in nested attributes works correctly.

Upvotes: 1

Views: 1151

Answers (1)

max
max

Reputation: 102036

The first step you need to take is to ensure uniqueness on the database level:

class AddUniquenessConstraintToProjectMemberships < ActiveRecord::Migration
  def change
    # There can be only one!
    add_index :project_memberships, [:user, :project], unique: true
  end
end

This avoids race conditions that would occur if we relied on ActiveRecord alone.

(C) Thoughtbot

From Thoughtbot: The Perils of Uniqueness Validations.

You then want to add an application level validation to avoid the ugly DB driver exceptions that occur if you violate the constraint:

class ProjectMembership < ActiveRecord::Base
  belongs_to :user
  belongs_to :project
  validates :user, presence: true
  validates :project, presence: true 
  validates_uniqueness_of :user_id, scope: :project_id
end

You can then remove the -> { uniq } lambda on your associations as you have taken the proper steps to ensure uniqueness.

The rest of your issues are due to a misunderstanding of how accepts_nested_attributes_for works:

For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.

So {"user":{"project_memberships_attributes":[{"project_id": 1}]} will always create a new record if you do not have proper uniqueness validations.

Upvotes: 1

Related Questions