has_many through association dependent destroy under condition of who called destroy - ruby-on-rails

Is there a way to check, within a before_destroy hook, what object (class) called destroy?
In the following example, when a patient is destroyed, so are their appointments (which is what I want); however I don't want to allow a physician to be destroyed if there are any appointments associated with that physician.
Again, is there a way to do such a check in the before_destory callback? If not, is there any other way to accomplish this "destruction check" based on the "direction" of the call (i.e. based on who called)?
class Physician < ActiveRecord::Base
has_many :appointments, dependent: :destroy
has_many :patients, through: :appointments
end
class Patient < ActiveRecord::Base
has_many :appointments, dependent: :destroy
has_many :physicians, through: :appointments
end
class Appointment < ActiveRecord::Base
belongs_to :patient
belongs_to :physician
before_destroy :ensure_not_referenced_by_anything_important
private
def ensure_not_referenced_by_anything_important
unless patients.empty?
errors.add(:base, 'This physician cannot be deleted because appointments exist.')
false
end
end
end

Note that dependent: :destroy on a has_many :through relationship only deletes the association and not the associated record (i.e. the join records will be deleted, but the associated records won't). So if you delete a patient it will only delete the appointment and not the physician. Read the detailed explanation in the API docs.
I have pasted the relevant paragraphs below.
What gets deleted?
There is a potential pitfall here: has_and_belongs_to_many and has_many :through associations have records in join tables, as well as the associated records. So when we call one of these deletion methods, what exactly should be deleted?
The answer is that it is assumed that deletion on an association is about removing the link between the owner and the associated object(s), rather than necessarily the associated objects themselves. So with has_and_belongs_to_many and has_many :through, the join records will be deleted, but the associated records won’t.
This makes sense if you think about it: if you were to call post.tags.delete(Tag.find_by_name('food')) you would want the food tag to be unlinked from the post, rather than for the tag itself to be removed from the database.

Just say:
class Physician < ActiveRecord::Base
has_many :appointments, dependent: :restrict_with_exception
has_many :patients, through: :appointments
end
Note the dependent: :restrict_with_exception. This will cause Active Record to refuse to destroy any Physician records that have associated Appointment records.
See the API docs and the association basics guide.

Related

How to delete all the associations of a model when deleting it in Rails 5

I have 3 models in my project. User, Meal & Food.
A user has many meals. A meal can have many food items and a food item can be a part of many meals.
The user and the meal model are in a has_many association, while the meal and the food model are in a has_many :through association. The join model for the meal and the food model is called MealFood.
When deleting the user I have made it so that it deletes all of the meals that belong to the user. However I can't make it so that it also deletes all the associations of the meal that belong to the user.
I need to delete every record in the meal_foods table where the meal_id belongs to the user that is being deleted.
User Model
class User < ApplicationRecord
has_many :meals, :dependent => :delete_all
end
Meal Model
class Meal < ApplicationRecord
belongs_to :user, optional: true
has_many :meal_foods, :dependent => :delete_all
has_many :foods, through: :meal_foods
end
Food Model
class Food < ApplicationRecord
has_many :meal_foods
has_many :meals, through: :meal_foods
end
MealFood Model
class MealFood < ApplicationRecord
belongs_to :meal
belongs_to :food
end
Thanks in advance!
You probably want dependent: :destroy, not dependent: :delete_all. :delete_all won't run callbacks and that's likely why your deeper associations remain persisted.
See the docs here:
For has_many, destroy and destroy_all will always call the destroy
method of the record(s) being removed so that callbacks are run.
However delete and delete_all will either do the deletion according to
the strategy specified by the :dependent option, or if no :dependent
option is given, then it will follow the default strategy. The default
strategy is to do nothing (leave the foreign keys with the parent ids
set), except for has_many :through, where the default strategy is
delete_all (delete the join records, without running their callbacks).
This thread has better answers.

How it works - `belongs_to :user, dependent: :destroy`

I know how to work has_many :posts, dependent: :destroy. If User or something that has_many posts is destroyed, all belonging posts are also destroyed.
But what happens when Post model belongs_to :user, dependent: :destroy?
I found the option in Rails Guide, but I couldn't find out how to work it.
http://guides.rubyonrails.org/association_basics.html
"has_many"
A teacher "has_many" students. Every student has only one teacher, but every teacher has many students. This means that there is a foreign key, or teacher_id on the student referencing to what teacher they belong to.
"belongs_to"
A student "belongs_to" a teacher. Every teacher has many students, but every student has only one teacher. Again, there is the foreign key on the student referencing to what teacher they belong.
Let's work this out an using this student / teacher concept.
Teacher Model
class Teacher < ActiveRecord::Base
has_many :students, dependent: :destroy
end
Student Model
class Student < ActiveRecord::Base
belongs_to :teacher
end
Assuming these models then
Teacher.destroy
Will delete the instantiated teacher and all the students that were associated to that teacher.
For example
Teacher.find(345).destroy
Would destroy the record of the teacher with the ID of 345 and destroy all the associated students with that teacher.
Now to the heart of the question, what happens when my models look like this?
Teacher Model
class Teacher < ActiveRecord::Base
has_many :students, dependent: :destroy
end
Student Model
class Student < ActiveRecord::Base
belongs_to :teacher, dependent: :destroy
end
If I were to call
Student.destroy
This would destroy the instantiated student and that student's associated teacher. However to my knowledge (and according to the docs) this would not destroy any other students related to that teacher, leaving them 'orphaned'.
Here is a quote from the Ruby docs on this 1
If set to :destroy, the associated object is destroyed when this object is. This option should not be specified when belongs_to is used in conjunction with a has_many relationship on another class because of the potential to leave orphaned records behind.
It instantiates an instance of all of the users then sends the message destroy to each. Results in normal destruction lifecycle for the users that are destroyed this way.
I have try set dependent: :destroy into belongs_to. Example
class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy
end
class Comment < ActiveRecord::Base
belongs_to :post, dependent: :destroy
end
In a console, if comment has been destroy, post will be destroy
But I think you shouldn't set belongs_to + dependent: :destroy together
In fact, example on facebook, when 1 comment of 1 post has been deleted, this post will not be deleted

Rails 4: counter_cache in has_many :through association with dependent: :destroy

Although similar questions have already been asked:
counter_cache with has_many :through
dependent => destroy on a "has_many through" association
has_many :through with counter_cache
none of them actually addresses my issue.
I have three models, with a has_many :through association :
class User < ActiveRecord::Base
has_many :administrations
has_many :calendars, through: :administrations
end
class Calendar < ActiveRecord::Base
has_many :administrations
has_many :users, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
The join Administration model has the following attributes:
id
user_id
calendar_id
role
I would like to count how many calendars each user has and how many users each calendar has.
I was going to go with counter_cache as follows:
class Administration < ActiveRecord::Base
belongs_to :user, counter_cache: :count_of_calendars
belongs_to :calendar, counter_cache: :count_of_users
end
(and, of course, the corresponding migrations to add :count_of_calendars to the users table and :count_of_users to the calendars table.)
But then, I stumbled upon this warning in Rails Guides:
4.1.2.4 :dependent
If you set the :dependent option to:
:destroy, when the object is destroyed, destroy will be called on its associated objects.
:delete, when the object is destroyed, all its associated objects will be deleted directly from the database without calling their
destroy method.
You should not specify this option on a belongs_to association that is
connected with a has_many association on the other class. Doing so can
lead to orphaned records in your database.
Therefore, what would be a good practice to count how many calendars each user has and how many users each calendar has?
Well, dependent: :destroy will destroy the associated records, but it won't update the counter_cache, so you may have wrong count in counter_cache. Instead you can implement a callback that will destroy the associated records, and update your counter_cache.
class Calendar < ActiveRecord::Base
has_many :administrations
has_many :users, through: :administrations
before_destroy :delete_dependents
private
def delete_dependents
user_ids = self.user_ids
User.delete_all(:calendar_id => self.id)
user_ids.each do |u_id|
Calendar.reset_counters u_id, :users
end
end
end
And similarly, implement this for User model too

Rails isn't running destroy callbacks for has_many through join model

I have two AR models and a third has_many :through join model like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies, through: :ratings
end
class Movie < ActiveRecord::Base
has_many :ratings
has_many :users, through: :ratings
end
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :movie
after_destroy do
puts 'destroyed'
end
end
Occasionally, a user will want to drop a movie directly (without directly destroying the rating). However, when I do:
# puts user.movie_ids
# => [1,2,3]
user.movie_ids = [1, 2]
the rating's after_destroy callback isn't called, although the join record is deleted appropriately. If I modify my user model like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies,
through: :ratings,
before_remove: proc { |u, m| Rating.where(movie: m, user: u).destroy_all }
end
Everything works fine, but this is really ugly, and Rails then tries to delete the join model a second time.
How can I use a dependent: :destroy strategy for this association, rather than dependent: :delete?
Answering my own question, since this was difficult to Google, and the answer is super counter-intuitive (although I don't know what the ideal interface would be).
First, the situation is described thoroughly here: https://github.com/rails/rails/issues/7618. However, the specific answer is buried about halfway down the page, and the issue was closed (even though it is still an issue in current Rails versions).
You can specify dependent: :destroy for these types of join model destructions, by adding the option to the has_many :through command, like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies,
through: :ratings,
dependent: :destroy
end
This is counter-intuitive because in normal cases, dependent: :destroy will destroy that specific association's object(s).
For example, if we had has_many :ratings, dependent: :destroy here, all of a user's ratings would be destroyed when that user was destroyed.
We certainly don't want to destroy the specific movie objects here, because they may be in use by other users/ratings. However, Rails magically knows that we want to destroy the join record, not the association record, in this case.

HMT collection_singular_ids= deletion of join models is direct, no destroy callbacks are triggered

Just ran into an issue with a has_many :through association and after/before-destroy callbacks not being triggered.
Say, I have users, groups, and an intermediate relation called membership.
I have a form that allows users to be enrolled into groups by creating a new membership record when they check off associated checkboxes. Basically an array of group_ids.
Looks something like this:
Which group would you like to join? (check all that apply)
[] Group A
[] Group B
[] Group C
And I wish to record actions such as joining a group or leaving a group to activity log table and do some other less important thigns.
I have the following defined:
class Group < AR::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Membership < AR::Base
belongs_to :user
belongs_to :group
after_create :log_event_to_audit_table
after_destroy :log_event_to_audit_table
end
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
attr_accessible :group_ids # enables mass-assignment
end
When a new membership record is created the after_create is run as expected. However, the after_destroy does not get triggered!
After google-ing and read up the docs I discovered the reason why:
"Automatic deletion of join models is
direct, no destroy callbacks are
triggered" - from Ruby Guides.
Hmmmmmm...
So the join model's (in this case Membership's) destroy callbacks are not being triggered. Well that's a downer. Any reason as to why?
So my question what is the best way to work around this issue?
Should I define my own membership_ids= method in User model that calls membership.destroy directly?
Open to any suggestions about the best practices in such a scenario.
Thanks!
After carefully examining the API docs, it turns out has_many and has_and_belongs_to_many ("HABTM") have a few options just for this case:
before_add
after_add
before_remove
after_remove
class User < ActiveRecord::Base
has_many :groups, :through => :memberships, :after_remove => :your_custom_method
end
Judging by how many responses I got, this must not be a very well documented/used feature.
Just noting it here for myself and others who may stumble like I did.
I've struggled with the same problem recently and solved it by extending association and overriding its delete method:
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships do
def delete(*args)
groups = args.flatten
# destroy memberships in order to trigger their callbacks:
proxy_association.owner.memberships.where(group_id: groups).destroy_all
super
end
end
...
end
adding dependent: :destroy to the has many relationship actually calls the before_destroy and after_destroy methods in the Membership class.
class User < ActiveRecord::Base
has_many :groups, through: :memberships, dependent: :destroy
end

Resources