Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 265
Jacob Brown Avatar asked Jun 03 '15 20:06

Jacob Brown


1 Answers

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.

like image 186
Jacob Brown Avatar answered Nov 14 '22 02:11

Jacob Brown