嵌套属性与has_many通过关联创建对象两次

时间:2016-03-18 19:02:03

标签: ruby-on-rails postgresql activerecord rails-api

我有Rails API应用程序,通过project_memberships表,用户和项目之间存在多对多的关系。

型号:

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

控制器:

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

问题在于,当我向PUT发送http://localhost:3000/users/1请求时,请使用以下json:

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

user.update user_params使用相同的user_idproject_id创建2个ProjectMembership记录。

  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

通过在嵌套属性中指定id来销毁和更新已存在的记录,可以正常工作。

1 个答案:

答案 0 :(得分:1)

您需要采取的第一步是确保数据库级别的唯一性:

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

如果我们单独依赖ActiveRecord,这可以避免出现竞争条件。

(C) Thoughtbot

来自Thoughtbot: The Perils of Uniqueness Validations

然后,您希望添加应用程序级别验证,以避免在违反约束时出现丑陋的数据库驱动程序异常:

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

然后,您可以删除关联中的-> { uniq } lambda,因为您已采取适当的步骤来确保唯一性。

您的其他问题是由于对accepts_nested_attributes_for工作原理的误解造成的:

  

对于没有id密钥的每个哈希,新记录将是   实例化,除非哈希还包含_destroy密钥   评估为真。

如果您没有适当的唯一性验证,{"user":{"project_memberships_attributes":[{"project_id": 1}]}将始终创建新记录。