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.
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.