HABTM and counter_cache

As you might have noticed, has_and_belongs_to_many (HABTM) relationship in Ruby on Rails does not support the counter_cache.

Counter caching is a good way of improving performance by caching the number of child records associated with the parent in a parent table's column as opposed to querying the database each time you need that information. In a 1 to many (has_many) relationship, you can use the :counter_cache option on the belongs_to side of the matter to have counter cache updated automatically. With HABTM relationship, you need to do it a little bit differently.

One reason for this is that, of course, HABTM method doesn't support :counter_cache. Another reson is that with HABTM, you can do mass-associations so incrementing and decrementing counters by 1 simply does not make any sense.

Okay, so I'll assume you know how to put two models into a HABTM relationship using the join table. You can take a look at my previous post for a quick reminder.

So, let's assume we have a Post and Category models. Posts have and belong to many Categories, and vice versa. When you create the HABTM relationship using 'has_and_belongs_to_many' method it creates a bunch of helper methods for your coding pleasure, and also allows you to specify four callback methods using after_add, after_remove, before_add, and before_remove options. Well, we don't use them here. I just mentioned them for the fun of it. Seriously.

We can't use the callback methods specified via *_add and *_remove options, because those require both the Post and Category models to be saved, or else it will not work (for the counter_cache scenario, that is). To be precise, it won't work if you want to, say, nest the resources. Well, if you don't understand, just ignore this paragraph. I don't quite get it myself. The working solution is below anyway.

First, you need a counter cache column in one of the tables (or both, for that matter). When deciding which one should contain the counter you need to think about which will show the count information. For example, number of comments may be show along with a post, but it doesn't make sense to show the number of comments in the comments themselves. For our example, we'll show the number of posts in the category.

The Post model knows which categories need to update their counters, and each of the categories will do the updating for their own counter cache column. Let's start by creating the counter cache column. Create a migration and call it add_counter_cache_for_posts_to_categories.

class AddCounterCacheForPostsToCategories < ActiveRecord::Migration
def self.up
add_column :categories, :posts_count, :integer, :default => 0
Category.reset_column_information
Category.find(:all).each do |c|
c.update_attribute :posts_count, c.posts.length
end
end
def self.down
remove_column :categories, :posts_count
end
end

What this does is add a column to :categories table which is called :posts_count (this is our custom method so it doesn't need to be called that, but if I'm not mistaken, if you name the counter cache column like tablename_count, you will get some performance boost when calling parent.children.size which will read the cached value instead of actually querying the database). The column type is integer, of course, and we also pass a :default => 0 to set the default value, just in case.

After we have the column, we also update the column with actual data from the database, to initialize correct values for every category.

Next, go to the Post model file (post.rb) and add the following lines:

after_save :update_categories_counter_cache
def update_categories_counter_cache
self.categories.each { |c| c.update_count(self) } unless self.categories.empty?
end

What does all this mean? We are adding an after_save callback method called update_categories_counter_cache. This metod is triggered after the record is saved, and it loops through all categories (if self.categories array is not empty, that is) and calls update_count method for each of them. The update_count is defined in Category model file (well, not yet, but you will define it now). The reason for the loop is that you might have multiple assignments on one save, so we make sure all categories that the post belongs to are updated.

Let's define the update_count method for the Category model. Open category.rb and add the follwing:

def update_count
update_attribute(:posts_count, self.posts.length)
end

Now, save all the files, migrate the database, and you will have a counter_cache-like functionality for your HABTM models.

Reply

The content of this field is kept private and will not be shown publicly.

Powered by Drupal - Design by artinet