25
votes

I have a Task model associated to a Project model via has_many through and need to manipulate the data before delete/insert via the association.

Since "Automatic deletion of join models is direct, no destroy callbacks are triggered." i can not use callbacks for this.

In Task i need all project_ids to calculate a value for Project after Task is saved. How can i disable delete or change delete to destroy on has_many through association? What is best practise for this problem?

class Task
  has_many :project_tasks
  has_many :projects, :through => :project_tasks

class ProjectTask
  belongs_to :project
  belongs_to :task

class Project
  has_many :project_tasks
  has_many :tasks, :through => :project_tasks
4

4 Answers

59
votes

Seems like i have to use associations callbacks before_add, after_add, before_remove or after_remove

class Task
  has_many :project_tasks
  has_many :projects, :through => :project_tasks, 
                      :before_remove => :my_before_remove, 
                      :after_remove => :my_after_remove
  protected

  def my_before_remove(obj)
    ...
  end

  def my_after_remove(obj)
    ...
  end
end   
1
votes

This is what I did

in the model:

class Body < ActiveRecord::Base
  has_many :hands, dependent: destroy
  has_many :fingers, through: :hands, after_remove: :touch_self
end

in my Lib folder:

module ActiveRecord
  class Base
  private
    def touch_self(obj)
      obj.touch && self.touch
    end
  end
end
1
votes

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.

You can force Rails to 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 Task
  has_many :project_tasks
  has_many :projects, :through => :project_tasks,
            replace_with_destroy: true
  ...
end

class ProjectTask
  belongs_to :project
  belongs_to :task

  # any destroy callback in this model will be executed
  #...

end

class Project
  ...
end

With this, you ensure Rails invoke all the destroy callbacks. This could be very useful if you are using paranoia.

0
votes

It seems like adding dependent: :destroy to has_many :through relation will destroy the join model (instead of delete). This is because CollectionAssociation#delete internally refers to :dependent option to determine whether the passed records should be deleted or destroyed.

So in your case,

class Task
  has_many :project_tasks
  has_many :projects, :through => :project_tasks, :dependent => :destroy
end

should work.