Mongoid counter_cache with has_and_belongs_to_many

If I have two Mongoid models joined by a has_and_belongs_to_many relationship, Users and Groups. I need to quickly and efficiently sort by the most popular groups which really should be done with a counter_cache. However, counter_caches are implemented in rails by setting a callback to update the counter cache every time a child model is created/destroyed. But, Mongoid does not present callbacks for HABTM, and there are no real records that are getting created anyway. Additionally, you can add a whole collection of associations at once which would not be compatible with the efficient Mongoid inc operator. There is a mongoid_counter_cache gem that I played with for awhile, but for these reasons, I couldn’t get it to work with my existing schema, so I had to roll my own solution to this.

To complicate things, a user can follow a group or join a group. Following a group just keeps them informed on what the group is doing, joining allows them to participate. To make all this work, I have the following associations defined:

bc.
include Mongoid::Document
field :name, :type => String
field :type, :type => Symbol, :default => :custom
field :description, :type => String
index :name
index :type
field :follower_count
field :member_count

belongs_to :owner, :class_name => “User”, :inverse_of => :custom_groups

has_and_belongs_to_many :members, :class_name => “User”, :inverse_of => :joined_groups
has_and_belongs_to_many :followers, :class_name => “User”, :inverse_of => :followed_groups

And for the user:

bc.
has_and_belongs_to_many :joined_groups, :class_name => “PolcoGroup”, :inverse_of => :members
has_and_belongs_to_many :followed_groups, :class_name => “PolcoGroup”, :inverse_of => :followers

I’m sticking with the _count naming convention in case, I 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 and we also pass a :default => 0.

Since I can’t use Mongoid’s atomic update here, I’m going to go through the database and update the relevant field with the current count.

bq.
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

Leave a Reply