DRY association sweeping with acts_as_cached

Cherry picking exactly which models and associations to cache with Acts as Cached is dead simple.However, cached object associations quickly result in repetitive sweeping snippets.

How to cache associations

class Address < ActiveRecord::Base

  acts_as_cached :version => 1, :include => [:country] 
  belongs_to :addressable, :polymorphic => true #we can't do find :include => X with polymorhic associations
  belongs_to :country, :class_name => 'Globalize::Country', :foreign_key => 'country_id'

end

The above snippet will execute the following behind the scenes:

Address.get_cache(1) #Address.find(1, :include => [ :country ])

How to sweep associations

Overriding accessors

  class Address < ActiveRecord::Base

    def country
       Globalize::Country.get_cache( country_id ) unless country_id.nil?
    end

  end

Just works, always fresh but has the overhead of a ridiculous amount of CACHE.get lookups

Manually sweeping

  class Address < ActiveRecord::Base

    after_save :cache_sweeper
    after_destroy :cache_sweeper

    def cache_sweeper
       expire_cache
       country.expire_cache
    end

  end

and

  class Globalize::Country < ActiveRecord::Base

    has_many :addresses, :as => :addressable

    after_save :cache_sweeper
    after_destroy :cache_sweeper

    def cache_sweeper
       expire_cache
       addresses.each{|a| a.expire_cache }
    end

  end

Very bloated for complex relationships.

But there's a better way

Add the following to actsas_cached's InstanceMethods or Monkey Patch/moduleeval in your environment.

         def expire_cache_with_associations( *associations_to_sweep )
          ((self.class.cache_options[:include] || []) + associations_to_sweep).flatten.uniq.compact.each do |assoc|
            macro = self.class.reflect_on_association(assoc.to_sym).macro
            macro == :has_many ? self.send(assoc.to_sym).each{|r| r.expire_cache unless r.nil? } : self.send(assoc.to_sym).expire_cache unless self.send(assoc.to_sym).nil?
          end 
          expire_cache
        end

This snippet will sweep associations passed in the :includes config hash option, but also allows for additional associations to be merged in later on.

A DRY and refactored example

  class Address < ActiveRecord::Base
    acts_as_cached :version => 2, :include => [:country]

    #sweeps country    
    after_save :expire_cache_with_associations
    after_destroy :expire_cache_with_associations

  end

and

  class Globalize::Country < ActiveRecord::Base
    acts_as_cached :version => 1
    has_many :addresses, :as => :addressable

    after_save :cache_sweeper
    after_destroy :cache_sweeper

    #We don't cache all addresses with the country as one country can grow to a potentially
    #large number of adresses ... we just sweep to update the cached country for each address
    def cache_sweeper;  expire_cache_with_associations(:addresses) end

  end

Sweeping polymorphic associations

class Address < ActiveRecord::Base
  acts_as_cached :version => 1, :include => [:country] 
  belongs_to :addressable, :polymorphic => true 
  belongs_to :country, :class_name => 'Globalize::Country', :foreign_key => 'country_id'

  after_save     :cache_sweeper
  before_destroy :cache_sweeper

  protected  

  #sweep the related addressable entitiy
  def cache_sweeper; expire_cache_with_associations(:addressable) end

end

class Customer < ActiveRecord::Base
  acts_as_cached :version => 1, :include => [:address, :contact] 

  has_one :address, :as => :addressable, :dependent => :destroy  
  has_one :contact, :as => :contactable, :dependent => :destroy

  #nothing fancy here
  after_save     :expire_cache_with_associations
  before_destroy :expire_cache_with_associations
end  

class Contact < ActiveRecord::Base
  acts_as_cached :version => 1 
  belongs_to :contactable, :polymorphic => true

  after_save     :cache_sweeper
  before_destroy :cache_sweeper

  protected

  #sweep related contactable entitiy
  def cache_sweeper; expire_cache_with_associations(:contactable) end

end   

About this entry