Nested attributes with has_many through association creating object twice - ruby-on-rails

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.

Related

Why does ActiveRecord select every single association to update a single column?

Why does EVERY single belongs_to association need to be selected first just to call update!? This seems a bit ridiculous, and I don't remember this being like that before, maybe I was using a deprecated method or something.
I know there is update_attribute which doesn't have this problem, but I want to update multiple attributes at once using a bang method.
I have some records with 5 associations, and just to update one or two columns it automatically does...
SELECT * FROM a
SELECT * FROM b
SELECT * FROM c
SELECT * FROM d
SELECT * FROM e
UPDATE f
I also am not using any validations at all, nor validates_associated
Model:
class Lead < ApplicationRecord
belongs_to :organization
belongs_to :vendor
belongs_to :prospect
belongs_to :visit
belongs_to :session
has_one :result
enum status: [
'PENDING',
'COMPLETE',
]
end
and
lead = Lead.first
lead.update!(status: 1)
[8] pry(main)> s.update!(slug: 'x')
TRANSACTION (0.2ms) BEGIN
Campaign Load (0.5ms) SELECT "campaigns".* FROM "campaigns" WHERE "campaigns"."id" = $1 LIMIT $2 [["id", "76dfe777-f563-43b1-900b-a2c40ea7d072"], ["LIMIT", 1]]
Sequence Update (0.6ms) UPDATE "sequences" SET "slug" = $1, "updated_at" = $2 WHERE "sequences"."id" = $3 [["slug", "x"], ["updated_at", "2023-01-27 01:49:32.472637"], ["id", "19c41977-71c6-4c60-a6b6-7af6ce090d80"]]
TRANSACTION (0.7ms) COMMIT => true
All columns look like this:
t.references :prospect, null: false, foreign_key: true, type: :uuid
belongs_to associations are required by default, which automatically adds presence validation.
:required
When set to true, the association will also have its
presence validated. This will validate the association itself, not the
id. You can use :inverse_of to avoid an extra query during validation.
NOTE: required is set to true by default and is deprecated. If you
don't want to have association presence validated, use optional: true.
https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to
I don't know what :inverse_of is supposed to do in this situation, doesn't seem to do anything.
Let's say you have this model:
class Post < ApplicationRecord
belongs_to :user
end
belongs_to adds a presence validator:
>> Post.validators
=> [#<ActiveRecord::Validations::PresenceValidator:0x00007f1fa2e96c40 #attributes=[:user], #options={:message=>:required}>]
In short, it does this to validate user: post.user.blank?, which loads the association.
You can set association as optional:
class Post < ApplicationRecord
belongs_to :user, optional: true
end
>> Post.validators
=> []
and add your own validations if you want:
class Post < ApplicationRecord
belongs_to :user, optional: true
validates :user, presence: true, on: :create
validates :user_id, presence: true, on: :update
end
Which only sort of works and breaks in some situations.
It will probably be best to handle it outside of the model with a custom validator and just skip model validations.
To update multiple attributes:
post = Post.first
post.assign_attributes(title: "name")
post.save!(validate: false)
There is also a config:
https://guides.rubyonrails.org/configuring.html#config-active-record-belongs-to-required-by-default

How do you set up MTI in Rails with a polymorphic belongs_to association?

In an effort to create a Short, Self Contained, Correct (Compilable), Example, imagine that I want to do the following.
I have a blog website. There are two types of posts, TextPost and LinkPost. There are also two types of users, User and Guest. I would like to implement Multiple Table Inheritance with TextPost and LinkPost, by which I mean (hopefully I'm using the term correctly):
At the model level, I will have Post, TextPost and LinkPost. TextPost and LinkPost will inherit from Post.
At the database level, I will have tables for the "leaf" models of TextPost and LinkPost, but not for Post.
Each type of Post can belong to either a User or a Guest. So we have a polymorphic belongs_to situation.
My question is how to accomplish these goals.
I tried the following, but it doesn't work.
class Post < ApplicationRecord
self.abstract_class = true
belongs_to :author, polymorphic: true # user or guest
validates :title, :author_id, :author_type, presence: true
end
class TextPost < Post
validates :content, presence: :true
end
class LinkPost < Post
validates :url, presence: :true
end
class User < ApplicationRecord
has_many :text_posts, as: :author
has_many :link_posts, as: :author
validates :name, presence: true
end
class Guest < ApplicationRecord
has_many :text_posts, as: :author
has_many :link_posts, as: :author
end
class CreateTextPosts < ActiveRecord::Migration[6.1]
def change
create_table :text_posts do |t|
t.string :title
t.string :content
t.references :author, polymorphic: true
t.timestamps
end
end
end
class CreateLinkPosts < ActiveRecord::Migration[6.1]
def change
create_table :link_posts do |t|
t.string :title
t.string :url
t.references :author, polymorphic: true
t.timestamps
end
end
end
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
class CreateGuests < ActiveRecord::Migration[6.1]
def change
create_table :guests do |t|
t.timestamps
end
end
end
Console output:
:001 > user = User.create(name: 'alice')
(1.6ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
TRANSACTION (0.1ms) SAVEPOINT active_record_1
User Create (1.2ms) INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "alice"], ["created_at", "2021-06-11 23:33:38.445387"], ["updated_at", "2021-06-11 23:33:38.445387"]]
TRANSACTION (0.2ms) RELEASE SAVEPOINT active_record_1
:002'> text_post = TextPost.create(title: 'foo', content: 'lorem ipsum', author_id: 1, author_type:
'user')
Traceback (most recent call last):
1: from (irb):2:in `<main>'
NameError (wrong constant name user)
The names of constants look like the names of local variables, except that they begin with a capital letter.
All the built-in classes, along with the classes you define, have a corresponding global constant with the same name as the class called class name.
So in your case, when you define User class, there's a constant class name: User, but not user, that why the error NameError (wrong constant name user) is raised.
try text_post = TextPost.create(title: 'foo', content: 'lorem ipsum', author_id: 1, author_type: 'User')

Rails 6: Can't delete nested model. Random Insert statement

I using Rails 6 with Postgres and having issues deleting a nested model.
A random insert statement gets generated after the association has been deleted.
Let me explain my set up.
Migrations
class CreateEntries < ActiveRecord::Migration[6.0]
def change
create_table :entries do |t|
t.string :name
t.timestamps
end
end
end
class Cards < ActiveRecord::Migration[6.0]
def change
create_table :cards do |t|
t.string :card_number
t.belongs_to :entry, null: true, foreign_key: true
t.timestamps
end
end
end
Models
class Entry < ApplicationRecord
has_one :card, dependent: :destroy
accepts_nested_attributes_for :card, allow_destroy: true
end
class Card < ApplicationRecord
belongs_to :entry
end
Controller
class EntriesController < ApplicationController
before_action :set_entry
def update
#entry.update(entry_params)
end
def set_entry
#entry = Entry.find(params[:id])
end
def entry_params
params.require(:entry).permit(:name,
card_attributes: [:id, :card_number, :_destroy]
)
end
end
Request Params
Parameters: {"authenticity_token"=>"CQ...Ucw==", "entry"=>{"card_attributes"=>{"_destroy"=>"true"}}, "id"=>"1"}
These are the logs
(0.2ms) BEGIN
ConcessionCard Load (0.2ms) SELECT "cards".* FROM "cards" WHERE "cards"."entry_id" = $1 LIMIT $2 [["entry_id", 1], ["LIMIT", 1]]
Card Destroy (0.4ms) DELETE FROM "cards" WHERE "cards"."id" = $1 [["id", 2]]
Card Create (0.6ms) INSERT INTO "cards" ("entry_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["entry_id", 1], ["created_at", "2019-09-06 13:50:41.100718"], ["updated_at", "2019-09-06 13:50:41.100718"]]
(0.3ms) COMMIT
Why is insert being generated after the delete call? It's not even a rollback.
Note: I have tried both null:true and null:false in the Cards belongs_to migration. I also tried setting optional:true in the belongs_to :entry statement in the Card model
Unless you include an id in card_attributes then Rails sees this as a new record, so it just replaces the has_one with a newly created Card for you (which because of your dependent: :destroy option deletes the existing associated Card).
Best to use a form.fields_for :card block in your form partial/view, which will automatically add the hidden id tag for an existing Card.

Active Record : Validation failed on create instance

I'm trying to create a comment instance. It returns me a validation error.
A comment has one moderation and include reportable. So you can do moderation.reportable and it return comment.
I want the moderation instance to be create when a new comment is created.
class Comment < ApplicationRecord
include Reportable
after_create :create_moderation
def create_moderation
blog = self.blog
self.create_moderation!(blog: blog)
end
end
class Moderation < ApplicationRecord
belongs_to :reportable, foreign_key: "reportable_id", foreign_type: "reportable_type", polymorphic: true
...
end
module Reportable
extend ActiveSupport::Concern
included do
has_one :moderation, as: :reportable, foreign_key: "reportable_id", foreign_type: "reportable_type", class_name: "Moderation"
has_many :reports, through: :moderation
end
Failure/Error: self.create_moderation!(blog: blog)
ActiveRecord::RecordInvalid:
Validation failed: Reportable must exist
EDIT
Trying to add :
belongs_to :reportable, foreign_key: "reportable_id", foreign_type: "reportable_type", polymorphic: true, optional: true
and get :
ActiveRecord::NotNullViolation:
PG::NotNullViolation: ERROR: null value in column "reportable_id" violates not-null constraint
DETAIL: Failing row contains (2, 1, Comment, null, 0, null, 2017-12-01 09:02:11.81419, 2017-12-01 09:02:11.81419, Blog, unmoderate).
: INSERT INTO "moderations" ("blog_id", "reportable_type", "created_at", "updated_at", "blog_type") VALUES ($1, $2, $3, $4, $5) RETURNING "id"
Try optional: true in association. Something like below:
belongs_to :reportable, foreign_key: "reportable_id", foreign_type: "reportable_type", polymorphic: true, optional: true
Refer this. The optional: true is introduced in Rails 5.
EDIT
after_create :create_moderation
def create_moderation
blog = self.blog
self.create_moderation!(blog: blog)
end
I see the two method names are same, i.e., after comment creation, the create_moderation is called which again calls the create_moderation. Can you try changing the name of the method maybe?
ANOTHER SUGESSTION
Can you change the method to
def create_moderation
blog = self.blog
Moderation.create!(blog: blog, reportable: self)
end
or
def create_moderation
blog = self.blog
comment = self
comment.create_moderation!(blog: blog)
end
Do you still get the same error?
You can try below code:
class Comment < ApplicationRecord
include Reportable
before_create :create_moderation
def create_moderation
blog = self.blog
self.build_moderation(blog: blog)
end
end

Rails Nested Attributes properly assigning parent_id as index, but isn't assigning additional attributes

I've been working on implementing a new model that belongs to one of our web apps existing models. Eventually, I want to build out a nested form. I understand that the form, and strong params can have it's own suite of issues, but I am currently struggling to get the models to behave as I would expect in the rails console.
Rails 4.2.7, Postgres DB
UPDATE - 10/3/16 - Still trying to find the right solution, but have made some changes
Our work is with Schools and Districts, and this particular case deals with surveys and how a survey is assigned to a school and district. Until now, a survey has been assigned to a district with a SurveyAssignment model, and some down the line logic assumed that all schools in a district were also "assigned" to the survey. Now, we want to be able to add more granularity to the SurveyAssignment and allow some specificity at the school level.
So I created a SchoolSurveyAssignment model and started to get the bits in place.
Here is the relevant model info:
class District < ActiveRecord::Base
...
has_many :schools, dependent: :destroy
has_many :survey_assignments, dependent: :destroy
...
end
class School
...
belongs_to :district
has_many :school_survey_assignments
has_many :survey_assignments, :through => :school_survey_assignments
...
end
class SurveyAssignment
belongs_to :district
belongs_to :survey
has_one :survey_version, through: :survey
has_many :school_survey_assignments, inverse_of: survey_assignment
has_many :schools, :through => :school_survey_assignments
accepts_nested_attributes_for :school_survey_assignments
attr_accessor :survey_group, :survey_version_type, :survey_version_id, :school_survey_assignments_attributes
validates :survey_id, presence: true
end
class SchoolSurveyAssignment
belongs_to :survey_assignment, inverse_of: :school_survey_assignments
belongs_to :school
attr_accessor :school_id, :survey_assignment_id, :grades_affected, :ulc_affected
validates_presence_of :survey_assignment
validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
Relevant Controller code:
class SurveyAssignmentsController < ApplicationController
before_action :set_district
before_action :set_survey_assignment, only: [:show, :edit, :update, :destroy]
respond_to :html, :json, :js
def new
#new_survey_assignment = SurveyAssignment.new()
#district.schools.each do |school|
#new_survey_assignment.school_survey_assignments.build(school_id: school.id)
end
end
def create
#survey_assignment = SurveyAssignment.new(survey_assignment_params)
if #survey_assignment.save
flash[:notice] = "Survey successfully assigned to #{#district.name}"
else
flash[:alert] = "There was a problem assigning this survey to #{#district.name}"
end
redirect_to district_survey_assignments_path(#district)
end
def survey_assignment_params
params.require(:survey_assignment).permit(:survey_id, :status, :survey_version_id, school_survey_assignments_attributes: [:id, :survey_assignment_id, :school_id, grades_affected: [], ulc_affected: []]).tap do |p|
p[:district_id] = #district.id
p[:school_year] = session[:selected_year]
end
end
def set_district
#district = District.find(params[:district_id])
end
Here is the relevant schema info:
create_table "school_survey_assignments", force: :cascade do |t|
t.integer "survey_assignment_id"
t.integer "school_id"
t.integer "grades_affected", default: [], array: true
t.string "ulc_affected", default: [], array: true
end
add_index "school_survey_assignments", ["school_id"], name: "index_school_survey_assignments_on_school_id", using: :btree
add_index "school_survey_assignments", ["survey_assignment_id"], name: "index_school_survey_assignments_on_survey_assignment_id", using: :btree
create_table "survey_assignments", force: :cascade do |t|
t.integer "district_id"
t.integer "survey_id"
t.integer "status"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "school_year"
t.integer "last_response_status_id"
end
add_index "survey_assignments", ["district_id"], name: "index_survey_assignments_on_district_id", using: :btree
Once these were in place, I stepped into my rails console and attempted the following:
2.3.1 :002 > sa1 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
(0.2ms) BEGIN
SQL (0.7ms) INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:20.205144"], ["updated_at", "2016-09-30 21:30:20.205144"]]
(7.2ms) COMMIT
=> #<SurveyAssignment id: 369, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:20", updated_at: "2016-09-30 21:30:20", school_year: 2017, last_response_status_id: nil>
2.3.1 :003 > sa2 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
(0.3ms) BEGIN
SQL (0.4ms) INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:30.701197"], ["updated_at", "2016-09-30 21:30:30.701197"]]
(0.5ms) COMMIT
=> #<SurveyAssignment id: 370, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:30", updated_at: "2016-09-30 21:30:30", school_year: 2017, last_response_status_id: nil>
So now, I've successfully created two Survey Assignments. I'm now going to create two School Survey Assignments off of sa1:
2.3.1 :004 > [{school_id: 5}, {school_id: 6}].each do |ssa|
2.3.1 :005 > sa1.school_survey_assignments.create(ssa)
2.3.1 :006?> end
(0.2ms) BEGIN
SchoolSurveyAssignment Exists (2.4ms) SELECT 1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 5 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
SQL (0.4ms) INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id" [["survey_assignment_id", 369]]
(6.4ms) COMMIT
(0.6ms) BEGIN
SchoolSurveyAssignment Exists (0.4ms) SELECT 1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 6 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
SQL (0.3ms) INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id" [["survey_assignment_id", 369]]
(0.4ms) COMMIT
=> [{:school_id=>5}, {:school_id=>6}]
2.3.1 :007 > sa1.save
(0.3ms) BEGIN
(0.4ms) COMMIT
=> true
Now, it looks like I've successfully created two SchoolSurveyAssignments with survey_assignment_id = 369 and school_ids = 5 and 6
2.3.1 :008 > sa1.school_survey_assignments
SchoolSurveyAssignment Load (0.3ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."survey_assignment_id" = $1 [["survey_assignment_id", 369]]
=> #<ActiveRecord::Associations::CollectionProxy [#<SchoolSurveyAssignment id: 5, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>, #<SchoolSurveyAssignment id: 6, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>]>
As you can see from the ActivRecord::Associations::CollectionProxy, both of the SchoolSurveyAssignments were created, with survey_assignment_id: 369, but with a nil school_id. This is troubling as it seems to be
Ignoring the parameters being passed into the create function, and
ignoring the validation of school_id
Another item that I don't understand is the following:
2.3.1 :009 > SchoolSurveyAssignment.find(5).survey_assignment_id
SchoolSurveyAssignment Load (0.6ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1 [["id", 5]]
=> nil
2.3.1 :011 > SchoolSurveyAssignment.find(5).survey_assignment.id
SchoolSurveyAssignment Load (0.3ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1 [["id", 5]]
SurveyAssignment Load (0.4ms) SELECT "survey_assignments".* FROM "survey_assignments" WHERE "survey_assignments"."id" = $1 LIMIT 1 [["id", 369]]
=> 369
Calling .survey_assignment_id should return the attribute on the SchoolSurveyAssignment and give 369. .survey_assignment.id is simply just grabbing the parent object's ID. I would expect both to return the same value, but one returns nil.
The end use case is making a SurveyAssignment form that lets the user set the attributes for a new SurveyAssignment and also set the attributes for X number of SchoolSurveyAssignments (based on # of schools in a district; varies from 2 to 15). Once I get a better grasp on how these models are interacting, I feel confident in executing this goal, but the behavior I'm seeing doesn't make sense to me, and I was hoping to find some clarity on implementing these related models. I feel like I'm bouncing around the answer, but am missing a key detail.
Thanks,
Alex
Try removing your attr_accessor lines of code. attr_accessor shouldn't be used for attributes that are persisted in the database and it's probably messing up the methods that ActiveRecord already provides by default, causing those attributes to not be saved properly
class SurveyAssignment
belongs_to :district
belongs_to :survey
has_one :survey_version, through: :survey
has_many :school_survey_assignments, inverse_of: survey_assignment
has_many :schools, :through => :school_survey_assignments
accepts_nested_attributes_for :school_survey_assignments
validates :survey_id, presence: true
end
class SchoolSurveyAssignment
belongs_to :survey_assignment, inverse_of: :school_survey_assignments
belongs_to :school
validates_presence_of :survey_assignment
validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
For the first question, School and SurveyAssignment don't know each other, school_id becomes nil. In your app, these models have m-to-n association indirectly, so how about using has_many through association between models?
In School model, add below:
has_many :survey_assignments, :through => :school_survey_assignments
In SurveyAssignments model, add below:
has_many :schools, :through => :school_survey_assignments
For the last question, both codes seem to be same..

Resources