The after_commit callback is not being triggered when the has_many relationship is updated and a record is destroyed.
I have a relationship
class Expertise
has_many :doctor_expertises
has_many :doctor_profiles, through: :doctor_expertises
class DoctorExpertise
belongs_to :doctor_profile
belongs_to :expertise
after_commit :do_something
def do_something
# not called when record destroyed
end
in my controller I use the following method to update the has_many relationship
def create
doc = DoctorProfile.find(params[:doctor_id])
doc.expertise_ids = params[:expertise_ids].select do |x|
x.to_i > 0
end
doc.save!
render json: doc.expertises
end
I understand that I should be using update and destroy on the relationship. However, why is after_commit not getting called on a record when it is destroyed?
I'm guessing it has something to do with the way I'm setting doc.expertise_ids not triggering the callback. However I'm unable to find any documentation about this method except for briefly here. Is there documentation that confirms or denies this suspicion or is there something else going on?
From the RailsGuides that you linked:
Automatic deletion of join models is direct, no destroy callbacks are triggered.
Although it doesn't state about after_commit it's quite likely that isn't being fired too
I think the answer you are looking for is here:
Howto use callbacks in a has_many through association?
You need to user after_remove within the has_many declaration
Updating joins model associations, Rails add and remove records on the collection. To remove the records, Rails use the delete method and this one will not call any destroy callback.
Solution 1
One way to invoke some callbacks when add or remove association is using Association Callbacks.
class Expertise
has_many :doctor_expertises
has_many :doctor_profiles, through: :doctor_expertises
before_remove: :my_before_remove,
after_remove: my_after_remove
def my_before_remove(doctor_profile)
...
end
def my_after_remove(doctor_profile)
...
end
end
Solution 2
Force Rails call destroy instead delete when is removing records.
To do that, install the gem replace_with_destroy and pass the option replace_with_destroy: true to the has_many association.
class Expertise
has_many :doctor_expertises
has_many :doctor_profiles, through: :doctor_expertises,
replace_with_destroy: true
...
end
class DoctorExpertise
belongs_to :doctor_profile
belongs_to :expertise
after_commit :do_something
def do_something
# this will be called when updating Expertise.doctor_profiles
# because use destroyed instead delete to remove the record
end
With this, you ensure Rails invoke all the destroy callbacks.
Just add dependent: :destroy like so
has_many :doctor_profiles, through: :doctor_expertises, dependent: :destroy
I know it's somewhat misleading to have dependent destroy on has_many through, but it will trigger destroy callback and it will not destroy the base records, just the join records. Reference: https://github.com/collectiveidea/audited/issues/246
Related
I have the following model:
class PhoneNumber < ActiveRecord::Base
has_many :personal_phone_numbers, :dependent => :destroy
has_many :people, :through => :personal_phone_numbers
end
I want to set up an observer to run an action in a delayed_job queue, which works for the most part, but with one exception. I want the before_destroy watcher to grab the people associated with the phone number, before it is destroyed, and it is on those people that the delayed job actually works.
The problem is, when a phone number is destroyed, it destroys the :personal_phone_numbers record first, and then triggers the observer when it attempts to destroy the phone number. At that point, it's too late.
Is there any way to observe the destroy action before dependent records are deleted?
While this isn't ideal, you could remove the :dependent => :destroy from the personal_phone_numbers relationship, and delete them manually in the observer after operating on them.
However, I think that this issue might be showing you a code smell. Why are you operating on people in an observer on phone number. It sounds like that logic is better handled in the join model.
Use alias_method to intercept the destroy call?
class PhoneNumber < ActiveRecord::Base
has_many :personal_phone_numbers, :dependent => :destroy
has_many :people, :through => :personal_phone_numbers
alias_method :old_destroy, :destroy
def destroy(*args)
*do your stuff here*
old_destroy(*args)
end
end
It sounds like your problem in a nutshell is that you want to gather and act on a collection of Person when a PersonalPhoneNumber is destroyed. This approach may fit the bill!
Here is an example of a custom callback to collect Person models. Here it's an instance method so we don't have to instantiate a PersonalPhoneNumberCallbacks object in the ActiveRecord model.
class PersonalPhoneNumberCallbacks
def self.after_destroy(personal_phone_number)
# Something like people = Person.find_by_personal_phone_number(personal_phone_number)
# Delayed Job Stuff on people
end
end
Next, add the callback do your ActiveRecord model:
class PersonalPhoneNumber < ActiveRecord::Base
after_destroy PictureFileCallbacks
end
Your after_destroy callback will have the model passed down and you can act on its data. After the callback chain is complete, it will be destroyed.
References
http://guides.rubyonrails.org/active_record_validations_callbacks.html#relational-callbacks
http://guides.rubyonrails.org/association_basics.html#association-callbacks
You can use a before_destroy callback in the model, then grab the data and do whatever operation you need to before destroy the parent. Something like this example should be what you are looking for:
class Example < ActiveRecord::Base
before_destroy :execute_random_method
private
def execute_random_method
...
end
handle_asynchronously :execute_random_method
A bit old but thought I'd share that rails now has the nice 'prepend' option for the before_destroy callback now. This goes follows the same line of thought with tomciopp had but allows you to define the before_destroy whereever in the class.
before_destroy :find_associated_people, prepend: true
def find_associated_people
# using phone number, find people
end
I am using a has_many through association and having trouble getting the before_destroy call back to trigger. I am using a Relating class to relate models.
class Relating < ActiveRecord::Base
belongs_to :relater, :polymorphic => true
belongs_to :related, :polymorphic => true
before_destroy :unset_reminders
end
For example, a user can add TvShows to a list of favorites, User.rb:
has_many :tv_shows, :through => :relateds, :source => :related, :source_type => 'TvShow'
The problem I am having, has to do with deleting this Relating record.
I can relate users and tv shows by:
user = User.find(1)
show = TvShow.find(1)
user.tv_shows << show
But when I want to remove this association, the before_destroy is not triggered by:
user.tv_shows.delete(show)
However, if I destroy the relating record manually, it does trigger the callback:
r = Relating.find(8012)
r.destroy
How can I get the before destroy to be triggered for this?
Thanks
The delete method does not trigger callbacks as mentioned in the docs here. Try destroy instead.
Update: I didn't realize you were trying to destroy the join record and not the show itself. I'm surprised delete works at all but perhaps that is a feature of has_many :through. How about:
user.relateds.where(tv_show_id: show.id).destroy
What is the easiest way to implement soft-deletes on has_many through association?
What I want is something like this:
class Company > ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: {staffings: {active: true}}
end
I want to use Company#users the following way:
the Company#users should be a normal association so that it works with forms and doesn't break existing contract.
when adding a user to the company, a new Staffing with active: true is created.
when removing a user from a company, the existing Staffing is updated active: false (currently it just gets deleted).
when adding a previously removed user to the company (so that Staffing#active == false) the Staffing is updated to active: true.
I thought about overriding the Company#users= method, but it really isn't good enough since there are other ways of updating the associations.
So the question is: how to achieve the explained behaviour on the Company#users association?
Thanks.
has_many :through associations are really just syntactic sugar. When you need to do heavy lifting, I would suggest splitting up the logic and providing the appropriate methods and scopes. Understanding how to override callbacks is useful for this sort of thing also.
This will get you started with soft deletes on User and creating Staffings after a User
class Company < ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: ['staffings.active = ?', true]
end
class Staffing < ActiveRecord::Base
belongs_to :company
has_one :user
end
class User < ActiveRecord::Base
belongs_to :staffing
# after callback fires, create a staffing
after_create {|user| user.create_staffing(active: true)}
# override the destroy method since you
# don't actually want to destroy the User
def destroy
run_callbacks :delete do
self.staffing.active = false if self.staffing
end
end
end
I have the following model associations:
class Slider < ActiveRecord::Base
has_one :featured_happening, :as => :featured_item, :dependent => :destroy
before_destroy :destroy_featured_happening
after_create :create_featured_happening
end
class FeaturedHappening < ActiveRecord::Base
belongs_to :featured_item, :polymorphic => true
end
When I destroy a Slider object I thought the dependent => :destroy would automatically destroy the featured_item but it does not.
Slider controller
def destroy
slider = Slider.find(params[:id])
slider.delete
render(:status => :ok, :nothing => true )
end
So then I tried a callback with before_destroy to manually delete the featured_item when a slider object is destroyed and nothing is getting called.
How can I get the featured_item to be deleted when I delete a slider object? Using Rails 3.2.
You just observed the difference between delete and destroy. In your controller you call
slider.delete
which will just execute a SQL DELETE but will not call any callbacks. That's why in normal cases you want to use destroy instead. It will fetch the object (if necessary), call the callbacks including recursive destroys and only then deletes the object from the database.
See the documentation for delete and destroy for more information.
Ensure that you are calling Slider#destroy in order to trigger callbacks. Slider#delete will simply delete the record without calling them.
class Slider < ActiveRecord::Base
has_one :featured_happening, :dependent => :destroy
end
class FeaturedHappening < ActiveRecord::Base
belongs_to :featured_item
end
If your code was simple like above in which there is simple relationship. Then with the deletion of slider, Feature Happing will be delete with the deletion of slider. But in your case the relationship is polymorphic. In feature happing table in DB the abject_id will be the id of slider or may be some other class. Therefore the dependent :destroy not working. So you should need to delete it with before_destroy callback in which you first retrieve the records and then destroy them
I have a has_many :through model that works perfectly.
has_many :varietals
has_many :grapes, :through => :varietals, :dependent => :destroy
I would like to call another action instead of :destroy. In fact, I don't want to nullify the item OR destroy it, I want to update the record status field from 1 to 0 instead of destroy the record.
How to call a custom method instead of destroy ? I suppose I can do that in the model itself... Thanks.
Where to put this method ? In the master model or in the model where the record will be destroyed ?
EDIT:
I'm sorry but I think I didn't enough explain my problem. My problem is not only to so something after the master model is destroyed. I want to custom the destroy action in the Varietal model itself even if the master record is not destroyed.
Something like:
class Varietal < ActiveRecord::Base
private
def destroy
self.update_attributes(:status => 0)
end
end
Actually this action is not called...
You can use before_destroy to put your custom logic there. E.g.,
before_destroy :reset_status
def reset_status
...
end
Check here for more details.
You just need add a callback on before_destroy or after_destroy and manipulate your associations. By example
after_destroy :do_on_grapes
def do_on_grapes
grapes.map(&:to_do)
end
has_many :dependent is limited to only a few options. According to the documentation:
:dependent If set to :destroy all the associated objects are destroyed
alongside this object by calling their destroy method. If set to
:delete_all all associated objects are deleted without calling their
destroy method. If set to :nullify all associated objects’ foreign
keys are set to NULL without calling their save callbacks. If set to
:restrict this object raises an ActiveRecord::DeleteRestrictionError
exception and cannot be deleted if it has any associated objects.
If using with the :through option, the association on the join model
must be a belongs_to, and the records which get deleted are the join
records, rather than the associated records.
It looks like you would need to alter the destroy method to update the status field.
I believe that good approach to solve your problem is to provide a custom destroy method. There are several responses to questions like these, but you should keep in mind that ActiveRecord and Relationships like:
class Image < ActiveRecord::Base
has_many :comments, dependent: :destroy
use callback mechanisms that trigger destroy chaining to your relations, too. Usually you should preserve this mechanism and add it to your custom implementation. E.g.
def destroy
self.update deleted_at: Time.now
run_callbacks :destroy
end
You can read this post, too:
Triggering dependent: :destroy with overridden destroy-method