Reputation: 103
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
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.
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