Monday, September 5, 2011

Overriding Model.get in Backbone.js

‹prev | My Chain | next›

When I create a new appointment in my Backbone.js calendar, it saves just fine. If I reload the page or check in the CouchDB backend, the appointment persists. But, if I try to delete the appointment immediately after I create it, I get an error.

The specific error is an HTTP 409, which indicates some kind of conflict. In CouchDB, this usually means that I have forgotten to include a revision number. But wait... I am including the revision number on DELETE:
    window.Appointment = Backbone.Model.extend({
      // ...
      destroy: function() {
        Backbone.Model.prototype.destroy.call(this, {
          headers: {'If-Match': this.get("_rev")}
        });
      }
    });
In fact, that works when I delete a pre-existing record. It is just newly created records that throw me for a loop. So what gives?

The trouble turns out to be how CouchDB represents IDs, revisions and other meta data about the documents that it stores. On create, CouchDB returns:
{"ok":true,"id":"7acf98778a669f4d6fc33d6b340106de","rev":"1-21662a1368aa1592d1e5d1df710f6d8c"}
But the actual data is stored as:
{
   "_id": "7acf98778a669f4d6fc33d6b340106de",
   "_rev": "1-21662a1368aa1592d1e5d1df710f6d8c",
   "title": "Delete me #2",
   "description": "asdf",
   "startDate": "2011-09-15"
}
CouchDB normally represents meta data with a leading underscore ("_id", "_rev"). In the POST / create response, however, the ID and revision returned are not meta-data. Rather they are the actual data returned describing the newly created record.

The problem is that Backbone slurps the CouchDB response directly into the model's attributes. This means that appointment.get("_rev") will not work but appointment.get("rev") will.

Now, I could change my delete code to get _rev or rev:
      destroy: function() {
        Backbone.Model.prototype.destroy.call(this, {
          headers: {'If-Match': this.get("_rev") || this.get("rev")}
        });
      }
The problem with this approach is twofold. First, I have to remember to do this everywhere that I want to access the revision (which will definitely be necessary when I add updates). The other is that I have to remember CouchDB's meta-data policy any time I want to access these attributes.

I think, ideally, I would like to call this.get("rev") and it just work—regardless of update, create or delete.

This turns out to be relatively easy with the pseudo sub-class method override suggested in Backbone's documentation:

    window.Appointment = Backbone.Model.extend({
      get: function(attribute) {
        return Backbone.Model.prototype.get.call(this, attribute) ||
               Backbone.Model.prototype.get.call(this, "_" + attribute);
      },
      // ...
    });
In my new get() method, I call the get() method directly on the Backbone.Model.prototype. Since I am invoking it directly, as not as method on an instantiated object, I have to supply the object context to be used inside the method. After all, the get() method expects to be called on an object and, as such, it expects the this variable to refer to that object.

Not coincidentally, the Javascript call() method does just this—it sets the this variable inside the function to the first argument supplied. In this case, I supply the this variable from my Appointment model. So, in the end, the original get() is called with the same this variable with which it would have otherwise been called.

The difference is that I can make two calls—one with the normal attribute (e.g. "rev") and the second with an underscore prepended to it (e.g. "_rev").

With that, I can change my destroy() method to work with both CouchDB updates and deletes:

    window.Appointment = Backbone.Model.extend({
      get: function(attribute) {
        return Backbone.Model.prototype.get.call(this, attribute) ||
               Backbone.Model.prototype.get.call(this, "_" + attribute);
      },
      destroy: function() {
        Backbone.Model.prototype.destroy.call(this, {
          headers: {'If-Match': this.get("rev")
        });
      },
      // ...
    });
If I try to destroy a pre-existing record, the first Backbone.Model.prototype.get.call() in my get() will return undefined but the second one ("_rev") will return the current revision number for the records.

It I try to destroy a record created after page load, then the first Backbone.Model.prototype.get.call() will return the revision ID.

I do not how often I will be connecting to a CouchDB backend as I work with Backbone. Still, it is comforting knowing that workarounds like this are fairly straight-forward with Backbone.


Day #134

3 comments:

  1. This can't be right:

    return Backbone.Model.prototype.get.call(this, attribute) || Backbone.Model.prototype.get.call(this, "_" + attribute);

    If the first expression returns something "false" (e.g. 0), then the second expression will be used. This means that if a get( "param" ) returns 0, then get( "_param" ) will be used instead and I assume that it will return null.

    ReplyDelete
  2. Ah, good catch. I had not considered the case in which either could return false-y Javascript values. I should be checking the first for typeof=='undefined' or something similar.

    It ought to work if I reverse the two. The CouchDB meta-data fields starting with underscore will either be undefined or a SHA1 -- but never other kinds of false-y. Even so, I would be relying on subtle implementation details. I should switch over to check both against undefined.

    Thanks!

    -Chris

    ReplyDelete
  3. Wouldn't it be better if on Create you add the id and rev (as _id and _rev) to your model object?
    Can you override or extend that?

    ReplyDelete