Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't collection=objects on has_many :through trigger callbacks on the join model on deletion of the association?

The Rails 4 documentation says this regarding destroy callbacks on the join model for a has_many :through relationship:

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.

Thankfully it's documented at least, but I want to know why on earth this is the case? Hopefully there's a technical reason because otherwise it's just crazy!

In my case I had a has_and_belongs_to_many relationship on the join tables model off to another model. The records on that second join table would never be deleted when the associated records on the first join table were deleted. I resorted to this which feels hacky, and I have to repeat myself on each side of the :through relationship:

has_many :schools_templates, dependent: :destroy
has_many :templates, through: :schools_templates, before_remove: :remove_groups_school_templates

private

def remove_groups_school_templates(template)
  schools_templates.where(template: template).first.groups.clear
end

There's a validation to 'ensure' uniqueness on the join tables records between the two foreign keys, so that's why I can call first in the callback.

like image 427
Brendon Muir Avatar asked Oct 31 '22 14:10

Brendon Muir


2 Answers

So I ran into the same issue the other day. In my case I was doing something similar to what you were doing and was running into the same issue with the join table being deleted instead of destroyed.

I started looking through the code and I believe the documentation is just out of date. has_many_through_association

All you need to do is add the dependent: :destroy to the has_many :through relationship.

class User
  has_many :partnerships, dependent: :destroy
  has_many :partners, through: :partnerships, dependent: :destroy
end

The pain I was dealing with was:

user.partner_ids = [1,2,3]
#creates the relationships
user.partner_ids = []
#was deleting the records from partnerships without callbacks.

The dependent: :destroy on the partners relationship fixed that. Callbacks are now being run and things are good again.

like image 66
Eric Krause Avatar answered Nov 15 '22 05:11

Eric Krause


Typically if you want to delete something through has_many association you put dependent: :destroy there:

class User
  has_many :partnerships, dependent: :destroy
  has_many :partners, through: :partnerships
end

If you want to destroy partners as well as partnerships you have to add this dependency to Partnerships model:

class Partnership
  belongs_to :partner, dependent: :destroy
  belongs_to :user
end

When object is destroyed it calls destroy on every object where destroy dependency is provided. So User calls destroy on every Partnership, and every Partnership calls destroy on every Partner.

"Why it can not be used with through" - well, the answer is "since deletion is direct". I know it does not says much (for me either), but on the other hand as for me adding dependencies to objects which are not linked directly is a bad idea. Consider example above: if dependent - destroy will work on partners and destroy them – should it destroy join model as well? Certainly yes, because in other case it will lead to data corruption, but you may lose some data which could lay in the join model, so in some cases – no, you don't want to destroy join model. This means Rails team would have to add new parameter – delete_join to indicate if you want to save that model or not. And that's just a bad design, since we already have better approach – add dependencies in the join models.

like image 42
shlajin Avatar answered Nov 15 '22 05:11

shlajin