Counter for acts_as_taggable

While working on yet another RoR (Ruby on Rails) project (which I hope to make public soon), I came across the need to extend the acts_as_taggable plugin to allow me directly view the number of items tagged by a 'Tag' object. My first thought was to extend the plugin functions and add a custom SQL query which would allow me to specify an ID of the taggable object and in return get the list of tags with the extra field. Result:

SELECT tags.name, count(*) AS count FROM taggings, tags
WHERE taggings.tag_id = tags.id AND taggings.tag_id IN
       ( SELECT taggings.tag_id FROM taggings WHERE taggings.taggable_id = ModelID )
GROUP BY tags.name

This is a slightly simplified case because I'm only tagging one Model class hence I do not require extra checks for taggable_type, but you get the idea. However, while it works, it's not the prettiest solution either. Instead of a simple query I'm putting a lot of stress on the query optimizer here by requiring it to run a join and a nested query. "IN" statements are pure syntactic sugar, and you can rewrite the query above without it, but I left that to the query optimizer. (I'm going on a limb here and assuming that MySQL 4/5 optimizer is up to the task).Seeing how my first attempt worked out I started searching the RoR docs and sure enough, found exactly what I wanted: :tablename:_count. This is an automagic field which can be added to the parent class of the belongs_to relationship which will keep track of the number of children referring to it. Here is what we need to make it all happen:

--- tagging.rb ---
- belongs_to :tag
+ belongs_to :tag, :counter_cache => true

Note: make sure you have the :counter_cache => true in your model, otherwise it just won't do anything useful. Next, modify the SQL table structure and add the :tablename:_count column. Here is my migration file:

class ExtendTaggable < ActiveRecord::Migration
  def self.up
      add_column :tags, :taggings_count, :integer, :default => 0
  end

  def self.down
     remove_column :taggings_count
  end
end

Run rake and you're almost done. Now our Tag objects keep a cache count for the number of Taggings objects that refer to it. No need for convoluted SQL queries to extract same information and virtually no overhead. And last but not least, here is a quick demo function I used in my view template to show the results:

for tag in @model.tags
   tag_string <<  "#{tag.name} (#{tag.taggings_count}), "
end

tag_string.chop.chop

Simple and easy, just the way we like it in Rails!


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.