The counter cache ability of a model relationship is amongst the most useful in Rails’ Active Record repertoire. In it’s standard form it looks like this:

belongs_to :parent_model, :counter_cache => true

Together with the matching has_many assertion (Or another rule) in the parent model, along with an appropriately named column in the parent table (In the case of our example that would be comments_count.

The Problem

Sometimes it would be great to not count every child object, but only those satisfying specific conditions.. How about having a blog with comments, and marking those comments as spam or not spam. We don’t want to tell our user that there is 3 comments, only for them to see 2, since 1 is spam and not available for public viewing until it has been moderated. In this case we would a custom counter cache, which at a guess could look something like:

belongs_to :post, :counter_cache => true, :conditions => "self.is_spam = false"

Unfortunately that doesn’t work, as it would violate Rails’ rule of convention over configuration. However, there is a solution.

The Solution

The counter cache works by simply updating a parent model’s count of related objects after each save or destroy operation on the child model. If we delete a comment, we expect the counter cache of the appropriate parent object to be decremented by one.

We can use exactly the same methodology when it comes to keeping a custom counter cache. First up, create a new column in your parent table using migrations (You are using migrations right?). Name the column something appropriate, I’ll be using public_comments_count in my example.

Next, we want to add some methods to child model (comment in this case) to update our custom counter cache on each save or destroy. Using after_save and after_destroy methods, we can do this every time and object is manipulated in any way that might affect our counter cache. The code looks as follows:

def after_save
  self.update_counter_cache
end

def after_destroy
  self.update_counter_cache
end

def update_counter_cache
  self.post.public_comments_count = Comment.count( :conditions => ["is_spam = false AND post_id = ?",self.post.id])
  self.post.save
end

Thats it! Now everytime a comment is added, removed or modified, my custom counter cache will be updated.

Hope this helps some people who like me were perplexed by the lack of support for this in Rails.

Did you like my Ruby on Rails related article? Then why not recommend me on Working with Rails?