ruby-on-railsruby-on-rails-4has-many-throughcounter-cache

Rails 4: counter_cache in has_many :through association with dependent: :destroy


Although similar questions have already been asked:

none of them actually addresses my issue.

I have three models, with a has_many :through association :

class User < ActiveRecord::Base
  has_many :administrations
  has_many :calendars, through: :administrations
end

class Calendar < ActiveRecord::Base
  has_many :administrations
  has_many :users, through: :administrations
end

class Administration < ActiveRecord::Base
  belongs_to :user
  belongs_to :calendar
end

The join Administration model has the following attributes:

id
user_id
calendar_id
role

I would like to count how many calendars each user has and how many users each calendar has.

I was going to go with counter_cache as follows:

class Administration < ActiveRecord::Base
  belongs_to :user, counter_cache: :count_of_calendars
  belongs_to :calendar, counter_cache: :count_of_users
end

(and, of course, the corresponding migrations to add :count_of_calendars to the users table and :count_of_users to the calendars table.)

But then, I stumbled upon this warning in Rails Guides:

4.1.2.4 :dependent

If you set the :dependent option to:

  • :destroy, when the object is destroyed, destroy will be called on its associated objects.
  • :delete, when the object is destroyed, all its associated objects will be deleted directly from the database without calling their destroy method.

You should not specify this option on a belongs_to association that is connected with a has_many association on the other class. Doing so can lead to orphaned records in your database.

Therefore, what would be a good practice to count how many calendars each user has and how many users each calendar has?


Solution

  • Well, dependent: :destroy will destroy the associated records, but it won't update the counter_cache, so you may have wrong count in counter_cache. Instead you can implement a callback that will destroy the associated records, and update your counter_cache.

    class Calendar < ActiveRecord::Base
    
      has_many :administrations
      has_many :users, through: :administrations
    
    
      before_destroy :delete_dependents
    
      private
      def delete_dependents
        user_ids = self.user_ids
        User.delete_all(:calendar_id => self.id)
        user_ids.each do |u_id|
          Calendar.reset_counters u_id, :users
        end
      end
    end
    

    And similarly, implement this for User model too