Cascade delete in Ruby ActiveRecord models? - ruby-on-rails

I was following the screencast on rubyonrails.org (creating the blog).
I have following models:
comment.rb
class Comment < ActiveRecord::Base
belongs_to :post
validates_presence_of :body # I added this
end
post.rb
class Post < ActiveRecord::Base
validates_presence_of :body, :title
has_many :comments
end
Relations between models work fine, except for one thing - when I delete a post record, I'd expect RoR to delete all related comment records. I understand that ActiveRecords is database independent, so there's no built-in way to create foreign key, relations, ON DELETE, ON UPDATE statements. So, is there any way to accomplish this (maybe RoR itself could take care of deleting related comments? )?

Yes. On a Rails' model association you can specify the :dependent option, which can take one of the following three forms:
:destroy/:destroy_all The associated objects are destroyed alongside this object by calling their destroy method
:delete/:delete_all All associated objects are destroyed immediately without calling their :destroy method
:nullify All associated objects' foreign keys are set to NULL without calling their save callbacks
Note that the :dependent option is ignored if you have a :has_many X, :through => Y association set up.
So for your example you might choose to have a post delete all its associated comments when the post itself is deleted, without calling each comment's destroy method. That would look like this:
class Post < ActiveRecord::Base
validates_presence_of :body, :title
has_many :comments, :dependent => :delete_all
end
Update for Rails 4:
In Rails 4, you should use :destroy instead of :destroy_all.
If you use :destroy_all, you'll get the exception:
The :dependent option must be one of [:destroy, :delete_all, :nullify,
:restrict_with_error, :restrict_with_exception]

Related

rails nested attributes creating, but not updating and not deleting

I have following models:
class Company < ActiveRecord::Base
has_and_belongs_to_many :people
has_many :companies_people
accepts_nested_attributes_for :companies_people, allow_destroy: true, reject_if: :all_blank
end
class CompaniesPerson < ActiveRecord::Base
belongs_to :company
belongs_to :person
belongs_to :company_role
end
class Person < ActiveRecord::Base
end
class CompanyRole < ActiveRecord::Base
end
and I'm trying along with Company object to update it's companies_people associated objects. The issue I'm facing is that I can create new companies_people objects but not update or remove existing ones. And what is the most thrilling is that it's not another question about not permitted or missing :id and :_destroy params - I have those set up for sure, but still can't nor update nor delete an existing association.
Eg. this call which has a purpose of updating company_role_id from 1 to 2 is being totally ignored:
Company.first.update_attributes(companies_people_attributes: [{id: 1, person_id: 1, company_role_id: 2}])
ps. tested with Rails 4.2.4
It appeared that it happens due to this association declaration:
has_and_belongs_to_many :people
I had this relation defined as HABTM initially, but later as it often happens I needed to get access to the join table and created a corresponding model CompaniesPerson, but didn't update :people association to work via has_many through. And now I discovered that changing the above statement to
has_many :people, through: :companies_people
or just completely commenting it out fixes the issue with nested attributes not updating and not deleting. Wow, pretty unexpected.
I have had this problem. The cause is rails doesn't know that your record is already exists.
You just need to add :ID in your parameters and it works.

When parental record is deleted, are childrecord deleted automatically by accepts_nested_attributes_for?

Here is an example on Rails 3.2 api for accepts_nested_attributes_for:
class Book < ActiveRecord::Base
has_one :author
has_many :pages
accepts_nested_attributes_for :author, :pages
end
Our question is that if a book record is deleted, are child records of author and pages deleted automatically along with the book record? Or we have to explicitly delete the child records in controller.
No, you need to set the dependent key.
has_many :pages, dependent: :destroy
As the OP points out there is another option for delete_all. The difference is that delete_all won't fire the model's before_destroy callbacks, it will just wipe them from the database.
This is beneficial because it doesn't require the Rails to load DB objects into Ruby, which is slow, but it also deletes them regardless of your defined callbacks.

Proper way to delete has_many :through join records?

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.

ActiveRecord callbacks on associations that are replaced by a new collection

Callbacks on objects which are updated during removal from a relationship collection don't seem to be executing for me.
I have a model EntityLocation which serves as a polymorphic relationship between Entities (Users/Places/Things) and Locations (Zips, Addresses, Neighborhoods).
class EntityLocation < ActiveRecord::Base
belongs_to :entity, :polymorphic => true
belongs_to :location, :polymorphic => true
def after_save
if entity_id.nil? || location_id.nil?
# Delete me since I'm no longer associated to an entity or a location
destroy
end
end
end
For this example, lets assume that I have a "Thing" with a collection of "Locations", referenced by my_thing.locations. This returns a collection of Zip, Address, etc.
If I write the code
my_thing.locations = [my_thing.locations.create(:location => Zip.find(3455))]
then as expected a new EntityLocation is created and can be accurately referenced from my_thing.locations. However the problem is that the records which were previously contained within this collection are now orphaned in the database with a nil entity_id attribute. I'm trying to delete these objects in the after_save callback, however it's never getting executed on the old object.
I've also tried using an after_update, and after_remove and neither gets called on the old record. The newly created record after_save callback does get called as expected, but that doesn't help me.
Does Rails update the previously referenced object without executing the callback chain through active record? All ideas appreciated. Thank you.
Why does this need to be polymorphic? It seems that you could simply use has_many :through to model the many-to-many relationship.
Secondly, why not simply delete the join table row through the association with :dependent => :destroy? Then you don't need a custom callback
class Entity < ActiveRecord::Base
has_many :entity_locations, :dependent => :destroy
has_many :locations, :through => :entity_locations
end
class EntityLocation < ActiveRecord::Base
belongs_to :entity
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :entity_locations, :dependent => :destroy
has_many :entities, :through => :entity_locations
end
Now deleting from either side deletes the join table row as well.

dependent => destroy on a "has_many through" association

Apparently dependent => destroy is ignored when also using the :through option.
So I have this...
class Comment < ActiveRecord::Base
has_many :comment_users, :dependent => :destroy
has_many :users, :through => :comment_users
...
end
...but deleting a Comment does not result in the associated comment_user records getting deleted.
What's the recommended approach, then, for cascade deletes when using :through?
Thanks
Apparently :dependent is not ignored!
The real issue was that I was calling Comment.delete(id) which goes straight to the db, whereas I now use Comment.destroy(id) which loads the Comment object and calls destroy() on it. This picks up the :dependent => :destroy and all is well.
The original poster's solution is valid, however I wanted to point out that this only works if you have an id column for that table. I prefer my many-to-many tables to only be the two foreign keys, but I had to remove my "id: false" from the migration table definition for cascading delete to work. Having this functionality definitely outweighs not having an id column on the table.
If you have a polymorphic association, you should do what #blogofsongs said but with a foreign_key attribute like so:
class User < ActiveRecord::Base
has_many :activities , dependent: :destroy, foreign_key: :trackable_id
end

Resources