In my project there are many models with has_many association and dependant: :destroy flag. Also, each model have other belong_to associations with the dependant: :destroy flag. This models are nested between each other so when a destroy is executed on the top one, Rails triggers cascading destroy on children models.
Apart from that, models have callbacks that execute before_destroy.
The following represents what I described above:
class Model1Example < ActiveRecord::Base
has_many :model2_examples, :dependent => :destroy
belongs_to :other1_example, :dependent => :destroy
belongs_to :other2_example, :dependent => :destroy
end
class Model2Example < ActiveRecord::Base
belongs_to :model1_example
has_many :model3_examples, :dependent => :destroy
belongs_to :other3_example, :dependent => :destroy
belongs_to :other4_example, :dependent => :destroy
before_destroy :update_something_on_model1
before_destroy :check_some_inconsistence
end
class Model3Example < ActiveRecord::Base
belongs_to :model2_example
belongs_to :other5_example, :dependent => :destroy
belongs_to :other6_example, :dependent => :destroy
before_destroy :update_something_on_model2
before_destroy :check_some_inconsistence
end
Given that on average Model2Example holds about 100+ instances of Model3Example when the Model1Example destroy is triggered many SQL queries are triggered (10k+) because deletion is record by record and also all rules are executed for every instance...and this takes a lot more than what a user could wait for such a simple action.
I could fix this performance issue by using dependant: :delete_all on the has_many associations instead, because I don't really care that all this rules are executed when I trigger Model1Example destroy.
But the problem is that when I execute (from elsewhere in the app) a Model2Example destroy is in my interest that all rules are executed (specially Model3Example rules for each instance), and the previous mentioned approach brakes this.
Is there a "Rails way" to achieve a performance improvement for this case? Or should I just use SQL for Model1Example deletion?
Also, if I have to use this approach and I wanted to check some basic stuff before destroying Model1Example, where is the best place to do this validation? controller?
Related
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.
I have this model
class XmlImport < ActiveRecord::Base
belongs_to :video
belongs_to :user
has_many :events, through: :event_import_records, dependent: :destroy
has_many :event_import_records, dependent: :destroy
has_attached_file :xml
validates_attachment_content_type :xml, :content_type => ["text/xml"]
end
The :event_import_records entries are being destroyed. But the :events are not.
Is the dependent:destroy on the has_many through association valid?
Is there another way of writing it? If that is not correct
How can I destroy all the events associated to the XmlImport through the event_import_records?
You can find at the Rails API that: "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." I understand that it delete the joins records but not the associated by through.
If I were you, I try:
class EventImportRecord < ActiveRecord::Base
has_many :events, dependent: :destroy
end
If not work I swap the order of the has_many relations on the XmlImport model, because of "Note that :dependent is implemented using Rails' callback system, which works by processing callbacks in order. Therefore, other callbacks declared either before or after the :dependent option can affect what it does." Also find at the same page of the Rails API.
class Post < ActiveRecord::Base
has_many :posts_tags
has_many :tags, through: :posts_tags
end
class PostsTag < ActiveRecord::Base
belongs_to :post
belongs_to :tag
end
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, through: :posts_tags
end
When Post gets destroyed I want all of its associations to Tag deleted as well. I do NOT want validations on PostsTag model to run. I just want to deleted.
I've found that adding a dependent on the relationship to posts tags from the Post model works as I want: has_many :posts_tags, dependent: :delete_all.
However, the documentation on the subject seems to suggest that I should do this instead: has_many :tags, through: :posts_tags, dependent: :delete_all. When I do this, the Tag object gets destroyed and the join object remains.
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
For has_many, destroy will always call the destroy method of the record(s) being removed so that callbacks are run. However delete 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 :nullify (set the foreign keys to nil), except for has_many :through, where the default strategy is delete_all (delete the join records, without running their callbacks).
How can I have the default strategy actually used? If I leave :dependent off completely, no records are removed at all. And I cannot just indicate :dependent on a has_many relationship. Rails comes back and says "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict ({})".
If I don't specify :dependent on either of the relationships, it does NOT nullify the post_id on the PostsTag object as it seems to suggest
Perhaps I am reading this wrong and the approach that I found works is the correct way?
Your original idea of:
has_many :posts_tags, dependent: :delete_all
is exactly what you want. You do not want to declare this on the has-many-though association :tags, as that will destroy all associated Tags. What you want to delete is the association itself - which is what the PostTag join model represents.
So why do the docs say what they do? You are misunderstanding the scenario that the documentation is describing:
Post.find(1).destroy
Post.find(1).tags.delete
The first call (your scenario) will simply destroy the Post. That is, unless you specify a :dependent strategy, as I suggest you do. The second call is what the documentation is describing. Calling .tags.delete will not (by default) actually destroy the tags (since they are joined by has-many-through), but the associated join model that joins these tags.
I am trying to figure out how i can tell what has changed in an array in the after save callback. Here is an example of code i am using:
class User < ActiveRecord::Base
has_many :user_maps, :dependent => :destroy
has_many :subusers, :through => :user_maps, :dependent => :destroy
has_many :inverse_user_maps, :class_name => "UserMap", :foreign_key => "subuser_id"
has_one :parent, :through => :inverse_user_maps, :source => :user
after_save :remove_subusers
def remove_subusers
if self.subuser_ids_were != self.subuser_ids
leftover = self.subuser_ids_were - self.subuser_ids
leftover.each do |subuser|
subuser.destroy
end
end
end
end
class UserMap < ActiveRecord::Base
belongs_to :user
belongs_to :subuser, :class_name => "User"
end
I am removing the subusers with the after_save callback because i could not get the dependent destroy feature to work through user_maps. Does anyone have any ideas on a way to do this?
Thanks!
You can use the Dirty module accessors http://ar.rubyonrails.org/classes/ActiveRecord/Dirty.html as suggested in Determine what attributes were changed in Rails after_save callback?
In your case the handler you have for after_save will have access to subusers_change which is an array of two elements, first being the previous value and second being the new value.
although not strictly the answer to your question, I think you maybe able to get :dependent => :destroy working if you try the following...
class User < ActiveRecord::Base
has_many :user_maps, :dependent => :destroy
has_many :subusers, :through => :user_maps # removing the :dependent => :destroy option
end
class UserMap < ActiveRecord::Base
belongs_to :user
belongs_to :subuser, :class_name => "User", :dependent => :destroy # add it here
end
By moving the :dependent => :destroy option to the belongs_to association in the UserMap model you set up a cascading delete via the UserMap#destroy method. In other words, calling User#destroy will call UserMap#destroy for each UserMap record, which will in turn call sub_user.destroy for its sub_user record.
EDIT
Since the solution above didn't work, my next suggestion would be to add a callback to the user_maps association, however this comes with a warning that I will add after
class User < ActiveRecord::Base
has_many :user_maps, :dependent => :destroy, :before_remove => :remove_associated_subuser
def remove_associated_subuser(user_map)
user_map.subuser.destroy
end
end
WARNINGS
1) Using a before_remove callback will mean that the user_map.destroy function won't be called if there is an error with the callback
2) You will have to destroy your UserMap record using the method on the User class for example...
# this will fire the callback
u = User.first
u.user_maps.destroy(u.user_maps.first)
# this WONT fire the callback
UserMap.first.destroy
All things considered, this would make me nervous. I would first try modifying your code to make the associations a little less coupled to the same tables, so the :dependent => :destroy option can work, and if you can't do that, add a cascade delete constraint on to the database, at least then your associations will always be removed regardless of where / how you destroy it in your rails app.
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