counter_cache for MongoMapper

I’ve started playing with MongoMapper, and it’s quite excellent, but it does suffer very much from being young. There are lots of pieces missing that veterans of ActiveRecord will take for granted. I’ve been working around or patching them, for the most part, but I felt that my solution to :counter_cache deserved a post.

In short, I didn’t want to hack around with the MongoMapper associations code, so I just implemented my own little ride-along version.

module SecretProject
  module CounterCache
    module ClassMethods
      def counter_cache(field)
        class_eval <<-EOF
          after_create "increment_counter_for_#{field}"
          after_destroy "decrement_counter_for_#{field}"
        EOF
      end
    end

    module InstanceMethods
      def method_missing(method, *args)
        if matches = method.to_s.match(/^(in|de)crement_counter_for_(.*)$/) then
          dir = matches[1] == "in" ? 1 : -1
          parent_association = matches[2]
          if parent = self.send(parent_association) then
            name = "#{self.class.to_s.tableize}_count"
            if parent.respond_to?(name)
              parent.collection.update({:_id => parent._id}, {"$inc" => {name => dir}})
            end
          end
        else
          super
        end
      end
    end

    def self.included(receiver)
      receiver.extend         ClassMethods
      receiver.send :include, InstanceMethods
    end
  end
end

Throw that into your lib directory, load it with an initializer, and then you can use it something like so:

class Foo
  include MongoMapper::Document
  include SecretProject::CounterCache

  belongs_to :user
  counter_cache :user  # Will cause a foos_count field on the owning user to be maintained when a Foo is created or deleted.
end

This’ll only increment a counter if you’ve defined one on your parent object, via key :foos_count, Integer or similar, just so that it doesn’t go around updating every model you might associate it with.

Yay.