Making Mongoid Play Nice With Backbone.js

Backbone has some great features that make it dirt-simple to integrate with a Rails back end. For example, the Backbone models have a .fetch(), .save() and .destroy() method on them. These methods make a call back to your server, based on a url configured on the model. On the server side, you only need to implement the standard CRUD methods of the controller that you routed your Backbone model to, and you have a simple API to CRUD data from the user’s browser, to your back end, and back.

But there’s a problem with Mongoid (the document mapper I’m using for MongoDB) that prevents this from working, out of the box.

 

The Problem: id vs _id

Most implementations of ORM and ODM mechanisms in Rails use the standard “id” attribute on models, to represent the identity of the model. Backbone takes advantage of this knowledge and uses the presence of an id attribute to determine whether or not an object was created in javascript on the browser, or on the server and sent to the browser.

If you create an object in javascript, there won’t be an id, initially. When you call .save() on that object, Backbone will http “post” your object to the server, so that rails will create a new server side object with the data provided. Then, you serialize the object that was created back to json and return it to the calling browser. Backbone reads the id that is now present on the object and updates it’s JSON representation of the model with the id. When you call .save() on the Backbone model again, it sees the id field and does an http “put” to the server, to update the existing model on the server side.

Mongoid, in your rails code, uses a .id attribute to store a document’s identity. However, there’s a small problem with the way mongoid serializes the document. If you examine the database collection for your document, you’ll see that every mongoid document is stored with an _id instead of an id field. I’m not sure why mongoid does this, but it does. Since mongodb stores documents as BSON (binary JSON) documents, mongoid naturally has a built in mechanism to convert models to json documents. This mechanism is the same whether you are saving to the database or calling .to_json on the model to return it to the browser. The net result is that mongoid sends JSON documents with a _id in them, which Backbone does not recognize.

class Foo
  include Mongoid::Document
  field :bar
end

foo = Foo.new(:bar => "baz")
foo.to_json # => { _id: "(some uuid)", bar: "baz" }

Because mongoid sends an _id field instead of an id field, Backbone will never try to http “put” your models back to the server, for an update. It will always http “post” them to create a new one.

 

The Solution: Override Mongoid’s .as_json

The solution to making mongoid play well with Backbone, is to provide an id field in the json that mongoid sends to the browser. To do that, we can override the as_json method of the Mongoid::Document module. This method gets called to generate a json document from a rails model. I’m not actually sure if this is a method that gets before during the execution of to_json, or if to_json is just an alias. Either way, this is the method that you want to override, and here’s how to do it:

module Mongoid
  module Document
    def as_json(options={})
      attrs = super(options)
      attrs["id"] = attrs["_id"]
      attrs
    end
  end
end

In this code, calling the super method of to_json, to get the json representation of the document from mongoid, directly. Then we’re adding an id key with the same value as _id. This allows the json serialization to have an id field the way Backbone wants. Include this module in your rails app and every mongoid document that is serialized to json will include and id field (It will not save the model to the mongodb collection with an “id”, though). This lets mongoid play nice with backbone, and you can now call .save() on your Backbone models knowing that it will create or update your model correctly.

(Note that we’re not removing the _id field in this code, so we do end up with the id duplicated. I’m not sure if removing _id would cause any issues, and we didn’t want to go a lot of work to find out.)


Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Derick Bailey

Derick Bailey is an entrepreneur, problem solver (and creator? :P ), software developer, screecaster, writer, blogger, speaker and technology leader in central Texas (north of Austin). He runs SignalLeaf.com - the amazingly awesome podcast audio hosting service that everyone should be using, and WatchMeCode.net where he throws down the JavaScript gauntlets to get you up to speed. He has been a professional software developer since the late 90's, and has been writing code since the late 80's. Find me on twitter: @derickbailey, @mutedsolutions, @backbonejsclass Find me on the web: SignalLeaf, WatchMeCode, Kendo UI blog, MarionetteJS, My Github profile, On Google+.
This entry was posted in Backbone, Javascript, JSON, MongoDB, Mongoid, Rails, Ruby. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://craiggwilson.wordpress.com Craig Wilson

    Mongoid does this because every document in MongoDB has an _id attribute that indicates the documents identity.  It can be of any type, but if you save a document without specifying an _id attribute, the server will create one for you; now you’ve got an _id and an id.

  • http://www.domain-hosting-services.in domain and hosting

    Already I know that, backbone has some great features but now only I am reading that related blog. Thanks for sharing.

  • Bryan Bailliache

    On your BackBone model, tell BackBone what attribute the id is by setting “idAttribute” to the String value of the name of the attribute you’d rather use than the default of id.

    Bonus: when Put/Post to your back-end, the attribute is still their and more useful than translating it each way.

  • Anonymous

    thanks for sharing outside of the mongoid google groups.

    ref: https://groups.google.com/forum/#!topic/mongoid/9UInzwRmL_I

  • Myxsm

    dude, just use idAttribute: ‘_id’ in your backbone model.

    • http://www.facebook.com/mrluonghuy Huy Nguyen Luong

      Thanks a lot :)

  • Anonymous

    I came upon a related gotcha that some folks might run into. For nested models the as_json hook doesn’t trigger and thus no id is added.

        user.as_json(:include => { :emails => { :only => ["id"] } } )

    This will leave you with emails that don’t have an id in the resulting JSON. The reason is that “id” isn’t an actual attribute in the model – so the :only directive does not find it in the model’s attributes. You can work around this in the following way:

        user.as_json(:include => { :emails => { :methods => ["id"] } } )

    Notice I’ve changed “:only” to “:methods”. This forces the serializer to call “id” on the model, which will produce the desired result.

    • http://mutedsolutions.com Derick Bailey

      _id is how MongoDB works out of the box. There may be a way to change that, but I’ve never tried to.

      It turns out the easy way to fix this in Backbone, is to set the “idAttribute” for a given model, or globally. It’s been noted a few times in the comments already.

      Here’s how I’m doing it globally (all models) for a current ASP.NET MVC app that serializes everything to a capital “I” in “Id”:

      Backbone.Model.prototype.idAttribute = “Id”;

      and that takes care of it for all models. This would work well for “_id”, with MongoDB models, too.

      • Anonymous

        Thanks for the reply Derick.

        Yeah – I can definitely see the attraction of the Backbone-based change. I think it’s a great choice for a simple app that’s not planning to expose an externally-facing JSON API.

        My point might be more relevant to apps that serialize their objects into JSON both for external consumption and for their own Backbone app to use. In that case, if they’ve done the work to get “_id” changed to “id” for their API consumers, they’ll want to keep that convention for the Backbone apps to keep serialization simple.

        • http://mutedsolutions.com Derick Bailey

          ah – good point. i wasn’t even thinking about exposing a JSON end point for other applications to integrate with

  • http://www.dorajistyle.pe.kr/ 月風

    It works fine with Spine.js too. Thanks alot.