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!