before_destroy is not firing from update_attributes - ruby-on-rails

I have a student that has many courses. In the student#update action and form, I accept a list of course_ids. When that list changes, I'd like to call a certain function. The code I have does get called if the update_attributes creates a course_student, but does not get called if the update_attributes destroys a course_student. Can I get this to fire, or do I have to detect the changes myself?
# app/models/student.rb
class Student < ActiveRecord::Base
belongs_to :teacher
has_many :grades
has_many :course_students, :dependent => :destroy
has_many :courses, :through => :course_students
has_many :course_efforts, :through => :course_efforts
# Uncommenting this line has no effect:
#accepts_nested_attributes_for :course_students, :allow_destroy => true
#attr_accessible :first_name, :last_name, :email, :course_students_attributes
validates_presence_of :first_name, :last_name
...
end
# app/models/course_student.rb
class CourseStudent < ActiveRecord::Base
after_create :reseed_queues
before_destroy :reseed_queues
belongs_to :course
belongs_to :student
private
def reseed_queues
logger.debug "******** attempting to reseed queues"
self.course.course_efforts.each do |ce|
ce.reseed
end
end
end
# app/controllers/students_controller.rb
def update
params[:student][:course_ids] ||= []
respond_to do |format|
if #student.update_attributes(params[:student])
format.html { redirect_to(#student, :notice => 'Student was successfully updated.') }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #student.errors, :status => :unprocessable_entity }
end
end
end

It turns out that this behavior is documented, right on the has_many method. From the API documentation:
collection=objects
Replaces the collections content by deleting and adding objects as appropriate. If the :through option is true callbacks in the join models are triggered except destroy callbacks, since deletion is direct.
I'm not sure what "since deletion is direct" means, but there we have it.

When a record is deleted using update/update_attributes, it fires delete method instead of destroy.
#student.update_attributes(params[:student])
Delete method skips callbacks and therefore after_create / before_destroy would not be called.
Instead of this accepts_nested_attributes_for can be used which deletes the record and also support callbacks.
accepts_nested_attributes_for :courses, allow_destroy: true
#student.update_attributes(courses_attributes: [ {id: student_course_association_id, _destroy: 1 } ])

Accepts nested attributes requires a flag to trigger nested destroy. Atleast it did back when.

If you add dependent: :destroy, it will honour that. Note that if you are using has_many through:, one needs to add that option to both.
has_many :direct_associations, dependent: :destroy, autosave: true
has_many :indirect_associations, through: :direct_associations, dependent: :destroy
(I have used that in Rails 3, I bet it will work on Rails 4 too)

Should your CourseStudent specify belongs_to :student, :dependent => :destroy, it seems like a CourseStudent record would not be valid without a Student.
Trying to follow the LH discussion I linked to above, and this one I'd also try moving the before_destroy callback in CourseStudent below the belongs_to. The linked example demonstrates how the order of callbacks matters with an after_create, perhaps the same applies to before_destroy. And of course, since you're using Edge Rails, I'd also try the RC, maybe there was a bug they fixed.
Failing those things, I'd try to make a really simple Rails app with two models that demonstrates the problem and post it to Rails' Lighthouse.

Wasn't able to leave a comment, so I'll just add an answer entry.
Just encountered the same bug. After hours of trying to figure this issue out and an hour of google-ing, I stumbled to this question by chance. Along with the linked LH ticket, and quote from the API, it now makes sense. Thank you!
While googling, found an old ticket. Direct link does not work, but google cache has a copy.
Just check Google for cached version of dev.rubyonrails.org/ticket/7743
Seems like a patch never made it to Rails.

Related

Rails validation messages for has_many through

I'm having trouble accessing validation messages for a related model when saving. The setup is that a "Record" can link to many other records via a "RecordRelation" which has a label stating what that relation is, e.g. that a record "refers_to" or "replaces" another:
class Record < ApplicationRecord
has_many :record_associations
has_many :linked_records, through: :record_associations
has_many :references, foreign_key: :linked_record_id, class_name: 'Record'
has_many :linking_records, through: :references, source: :record
...
end
class RecordAssociation < ApplicationRecord
belongs_to :record
belongs_to :linked_record, :class_name => 'Record'
validates :label, presence: true
...
end
Creating the record in the controller looks like this:
def create
# Record associations must be added separately due to the through model, and so are extracted first for separate
# processing once the record has been created.
associations = record_params.extract! :record_associations
#record = Record.new(record_params.except :record_associations)
#record.add_associations(associations)
if #record.save
render json: #record, status: :created
else
render json: #record.errors, status: :unprocessable_entity
end
end
And in the model:
def add_associations(associations)
return if associations.empty? or associations.nil?
associations[:record_associations].each do |assoc|
new_association = RecordAssociation.new(
record: self,
linked_record: Record.find(assoc[:linked_record_id]),
label: assoc[:label],
)
record_associations << new_association
end
end
The only problem with this is if the created association is somehow incorrect. Rather than seeing the actual reason, the error I get back is a validation for the Record, i.e.
{"record_associations":["is invalid"]}
Can anyone suggest a means that I might get record_association's validation back? This would be useful information for a user.
For your example, I would rather go with nested_attributes. Then you should easily get access to associated record errors. An additional benefit of using it is removing custom logic you have written for such behavior.
For more information check documentation - https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Adding relationships with Has_Many Through. undefined local variable but everything works

I've seen the undefined local variable in many threads, also I have seen adding relationships a lot, so please read the problem before suggesting it's answered elsewhere. (of course, if you do find an answer elsewhere, I will be just as grateful)
The tables are Services and Checks (in the context of a garage, the service is the type and the checks are what need to be carried out for any particular service). This is many to many, so I've got a has many through relationship set up.
The problem is, although everything is working fine, I am still getting the error:
undefined local variable or method `check_ids'
Line:
#service.service_checks.build(check_id: check_ids, service_id: #service.id )
Any attempt I make to define the variable seems to stop the relationships from working. Which leaves me clueless as to how to get everything working in harmony.
Models:
class Service < ActiveRecord::Base
has_many :service_checks, :dependent => :destroy
has_many :checks, :through => :service_checks
#attr_accessor :check_ids
accepts_nested_attributes_for :checks
accepts_nested_attributes_for :service_checks
end
class Check < ActiveRecord::Base
has_many :service_checks
has_many :services, :through => :service_checks
belongs_to :check_cat
end
class ServiceCheck < ActiveRecord::Base
belongs_to :service, dependent: :destroy
belongs_to :check, dependent: :destroy
accepts_nested_attributes_for :check, :reject_if => :all_blank
self.primary_key = [:service_id, :check_id]
end
My update function in Service looks like this:
def update
respond_to do |format|
if #service.update(service_params)
# This builds the relationships
#service.service_checks.build(check_id: check_ids, service_id: #service.id )
format.html { redirect_to #service, notice: 'Service was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #service.errors, status: :unprocessable_entity }
end
end
end
# Never trust parameters from the scary internet, only allow the white list through.
def service_params
params.require(:service).permit(:name, :price, :check_ids => [])
end
And my form is simply:
<%= f.association :checks,
as: :check_boxes,
label_method: :name,
value_method: :id,
label: 'Checks' %>
I think you've got everything setup for this to work OK if you just remove the line for #service.service_checks.build(check_id: check_ids, service_id: #service.id ). The association build is already handled. And once you're past update everything has been saved already.
The reason you're getting an undefined variable error is because check_ids doesn't exist. You are in a context where the params array coming down from the form and the check ids are available in params[:service][:check_ids] if you needed to access them. Note also that if you had to call service_checks.build you would want to pass a single value to the check_id attribute, and not an array like params[:service][:check_ids].
But, again, I think what you really want to try is removing the build line.
It would appear I do not need this line at all.
#service.service_checks.build(check_id: check_ids, service_id: #service.id )
Due to the relationship defined in the model, Rails is magically able to do the rest automatically!

Having trouble with :dependent => :destroy and Instance variables

I have a project with many items; and it's :dependent => :destroy.
I'm trying to tell rails when calling callbacks (specifically the after_destroy of Item), to run ONLY if the Item is destroyed "alone", but all of the project is NOT being destroyed.
When the whole project is being destroyed, I actually don't need this after_destroy method (of Item) to run at all.
I don't want to do :dependent => :delete since the Item has many other associations connected to it (with :dependent => :destroy).
It works for me only with class variable, but I wish it would had worked with an instance variable:
class Project < ActiveRecord::Base
has_many :items, :dependent => :destroy
before_destroy :destroying_the_project
def destroying_the_project
# this is a class variable, but I wish I could had #destroying_me
# instead of ##destroying_me.
##destroying_me = true
end
def destroying_the_project?
##destroying_me
end
end
class Item < ActiveRecord::Base
belongs_to :project
after_destroy :update_related_statuses
def update_related_statuses
# I with I could had return if project.destroying_the_project?
# but since the callback gets the project from the DB, it's another instance,
# so the instance variable is not relevant here
return if Project::destroying_the_project?
# do a lot of stuff which is IRRELEVANT if the project is being destroyed.
# this doesn't work well since if we destroy the project,
# we may have already destroyed the suites and the entity
suite.delay.suite_update_status
entity.delay.update_last_run
end
end
The other option I can think of is remove the :dependent => :destroy and manually handle the destroy of the items inside the Project after_destroy method, but it seems too ugly as well, especially since Project has many item types with :dependent => :destroy that would have to shift to that method.
Any ideas would be appreciated
I hope that's not the best solution, but at least it works and doesn't introduce any global state via class variables:
class Project < ActiveRecord::Base
has_many :items
before_destroy :destroying_the_project
def destroying_the_project
Rails.logger.info 'Project#destroying_the_project'
items.each &:destroy_without_statuses_update
end
end
class Item < ActiveRecord::Base
belongs_to :project
after_destroy :update_related_statuses,
:unless => :destroy_without_statuses_update?
def update_related_statuses
Rails.logger.info 'Item#update_related_statuses'
end
def destroy_without_statuses_update
#destroy_without_statuses_update = true
destroy
end
def destroy_without_statuses_update?
!!#destroy_without_statuses_update
end
end
If you don't need to use callbacks when deleting the whole project, you could use delete_all instead of destroy:
Rails :dependent => :destroy VS :dependent => :delete_all

Avoiding `save!` on Has Many Through Association

I have a has_many through association with an attribute and some validations on the "join model". When I try to do something like #user.projects << #project and the association has already been created (thus the uniqueness validation fails), an exception is raised instead of the error being added to the validation errors.
class User
has_many :project_users
has_many :projects, :through => :project_users
class Project
has_many :project_users
has_many :users, :through => :project_users
class ProjectUser
belongs_to :user
belongs_to :project
# ...
if #user.projects << #project
redirect_to 'somewhere'
else
render :new
end
How can I create the association like I would with the << method, but calling save instead of save! so that I can show the validation errors on my form instead of using a rescue to catch this and handle it appropriately?
I don't think you can. From the API:
collection<<(object, …) Adds one or more objects to the collection by
setting their foreign keys to the collection’s primary key. Note that
this operation instantly fires update sql without waiting for the save
or update call on the parent object.
and
If saving fails while replacing the collection (via association=), an
ActiveRecord::RecordNotSaved exception is raised and the assignment is
cancelled.
A workaround might look like this:
if #user.projects.exists? #project
#user.errors.add(:project, "is already assigned to this user") # or something to that effect
render :new
else
#user.projects << #projects
redirect_to 'somewhere'
end
That would allow you to catch the failure where the association already exists. Of course, if other validations on the association could be failing, you still need to catch the exception, so it might not be terribly helpful.
Maybe you could try to add an validation to your project model, like:
validates :user_id, :uniqueness => {:scope => :user_id}, :on => :create
Not sure if that helps to avoid the save! method..
Try declaring the associations as
has_many :projects, :through => :project_users, :uniq => true
Checkout out section 4.3.2.21 in http://guides.rubyonrails.org/association_basics.html.

My "has_many through" join model has nil reference after saving

I'm trying to create an object and adding an existing object to a "has_many through" association, but after saving my object the reference to my newly created object is set to nil in the join model.
To be specific, I'm creating a Notification object and adding a pre-existing Member object to the Notification.members association. I'm using nested resources and I'm invoking the notification controller's new function using the following relative URL:
/members/1/notifications/new
After filling out the form and submitting, the create function is called, and from what I understand from the Rails Associations guide, section 4.3.3 "When are Objects Saved?", the members associations should be created in the database when the new notification object is saved:
"If the parent object (the one declaring the has_many association) is unsaved (that is, new_record? returns true) then the child objects are not saved when they are added. All unsaved members of the association will automatically be saved when the parent is saved."
After creating the notification object, the following record was created in the database:
select id, notification_id, notifiable_type, notifiable_id from deliveries;
1|<NULL>|Member|1
I worked around the problem by saving the notification object before adding the member object to the association. At first this seemed to be an ok solution for now, but I soon discovered that this has it's downsides. I don't want to save the notification without it's member association since I then have to write workarounds for my callbacks so that they don't start performing tasks on the not yet valid notification object.
What am I doing wrong here? All tips are appreciated. :D
Models
class Notification < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :members, :through => :deliveries, :source => :notifiable, :source_type => "Member"
has_many :groups, :through => :deliveries, :source => :notifiable, :source_type => "Group"
end
class Member < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
class Delivery < ActiveRecord::Base
belongs_to :notification
belongs_to :notifiable, :polymorphic => true
end
# Group is not really relevant in this example.
class Group < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
Controller
class NotificationsController < ApplicationController
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
#notification.members << #member
respond_to do |format|
if #notification.save
...
end
end
end
end
After posting a bug report, I got som help from one of the Rails gurus. In short, it's impossible to get this working the way I thought.
I decided to proceed with slightly more controller code, seems to be working just fine:
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
respond_to do |format|
if #notification.save
#member.notifications << #notification
#member.save
...

Resources