Mongo Mapper and hybrid deployments: a first hack

At work we’re using RoR atop MySQL to build our core web application. As with many companies, we’re realizing that certain types of scaling problems may be better solved in the NoSQL world. While looking into MongoDB, I’ve found it somewhat surprising that the two dominant Rails-ready adapters (mongo_mapper and Mongoid) don’t really answer the question of hybrid deployments. In fact, much of the “getting started” documentation includes a section on ripping out ActiveRecord (since you obviously won’t need it now that you’re playing with a new NoSQL data store).

Similarly, there are few, if any, stories of people migrating just parts of their schema into MongoDB. Either people aren’t doing this or nobody is blogging about it. I found several conversations about people just abandoning the idea and converting wholesale to MongoDB. Given the 40-minute time span between some of these postings, these are either the greatest coders ever or they’re converting toys/demos/explorations rather than years-old applications with dozens of tables and tens of thousands of regular users.

As I started to work on things, it became clear that the document oriented nature of MongoDB was going to necessitate some changes in how we do things. Gone (in some ways) are a number of the easy and familiar ActiveRecord methods for associations between records. This goes double for a system with some parts in a relational database and other parts in a document store. Rather than just typing belongs_to and moving on, you end up having to write your own accessors and mutators over and over again.

This, of course, is silly. I’ve only been coding Ruby for a little while but it was clear that I should be able to DRY this up a little. Plus mongo_mapper (which I’m currently tentatively useing) provides plugins as a way to provide core functionality, making it easy to extend.

I’m still not great at Ruby, Rails, or some of the idioms that are standard to development with this world, but it didn’t take me long to write something that provides belongs_to_ar, a convenience variation to a key that creates a new key field and automatically handles loading/storing references to ActiveRecord (the objects still in your relational database).

module MongoAdapt
  module ExternalKey

    def self.included(model)
      model.plugin MongoAdapt::ExternalKey
    end

    module ClassMethods
      def belongs_to_ar(name, *args)
        key :"#{name}_id", Integer
        create_link_methods_for(name)
      end

      private
        def create_link_methods_for(name)
          # See MongoMapper::Plugin::Keys
          class_name = name.to_s.classify
          key_name = :"#{name.to_s}_id"
          accessors_module.module_eval <<-end_eval
            def #{name}
              #{class_name}.find(read_key(:#{key_name}))
            end

            def #{name}=(value)
              write_key(:#{key_name}, value.id)
            end

            def #{name}?
              read_key(:#{key_name}).present?
            end
          end_eval

          include accessors_module
        end
    end
  end
end

MongoMapper::Document.append_inclusions(MongoAdapt::ExternalKey)

With this loaded, I can now do belongs_to_ar :user on a MongoMapper::Document and get a user_id field in the underlying document as well as user and user= methods that operate with User objects. This, of course, only handles a single direction for the relationship (going from a Mongo Mapper document to a ActiveRecord object).

I imagine I’ll refine this a bit further and post some more about the challenges of a hybrid deployment as I go along. For one thing, it needs a more Ruby-ish (what the hell is the equivalent term to python-ic?) name.