【问题标题】:Nested attributes with has_many through association creating object twice通过关联创建对象两次使用 has_many 嵌套属性
【发布时间】:2016-03-18 19:02:02
【问题描述】:

我的 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

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

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

user.update user_params 创建 2 个具有相同 user_idproject_id 的 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 来销毁和更新现有记录可以正常工作。

【问题讨论】:

  • 我要检查的第一件事是 config/routes.rb 是否设置正确,以便调用正确的 Controller#Action。
  • @JonathanSoifer 我在user.update 之前和之后放置了binding.pry 并且执行停止了,所以我猜路线是正确的。我在最后添加的 SQL 查询在两者之间被调用。

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


【解决方案1】:

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

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

这避免了如果我们单独依赖 ActiveRecord 会发生的竞争条件。

来自Thoughtbot: The Perils of Uniqueness Validations

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

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

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

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

对于每个没有 id 键的散列,一个新记录将是 实例化,除非哈希还包含一个 _destroy 键 计算结果为真。

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

【讨论】:

  • 如果您有一个包含数据的现有数据库,则需要在添加索引之前清除所有重复项。
  • “将始终创建新记录”是的,我知道,这就是我在这里所期待的。但它立即创建的不是一个,而是两个新记录,这就是问题所在。正如我所说,更新和删除(提供了 id)可以正常工作。至于关于数据库的部分 - 谢谢,非常有趣的东西,但它不能解决原因,只是它的结果(阻止数据库级别的第二个 sql 插入请求)。而且我需要在将来删除唯一性约束,因为我希望用户能够多次(在不同的时间段内)成为项目的成员。
猜你喜欢
  • 1970-01-01
  • 2018-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-25
  • 1970-01-01
  • 2013-03-06
  • 1970-01-01
相关资源
最近更新 更多