简体   繁体   中英

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.

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) 思想机器人

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM