Wednesday, November 9, 2011

CouchDB and Backbone-relational

‹prev | My Chain | next›

I ended last night's exploration of Backbone-relational making quite a bit of headway, but also a bug. The bug resulted in a duplicate set of empty models in a has-many relationship.

In my case, my Backbone.js calendar application has appointments. Each appointment has many people that are invited ("invitees"). When viewing an appointment on the 17th, there should be 3 invitees. And indeed they show up, but so do three uninvited, no-name phantoms:


To figure this out, I add debugger statements to my calendar application. Lots of debugger statements.

Eventually, I add a debugger statement to my collection's parse() method:

    Invitees = Backbone.Collection.extend({
      model: Models.Invitee,

      url: function( models ) {
        return '/invitees?' + ( models ? 'ids=' + _.pluck( models, 'id' ).join(',') : '' );
      },
      parse: function(response) {
        debugger;
        return _(response.rows).map(function(row) { return row.doc; });
      }
    });
There is nothing actually wrong with that parse statement. It converts the results of a CouchDB query into a list of attributes that can be used to build individual Invitee models. As can be seen from the parse() method, the CouchDB response contains a "rows" attribute with the actual invitee documents / attributes. The "rows" in that response contain several attributes. The only one that is needed to create an Invitee Backbone model is the "doc" attribute, which contains the entire JSON representation of a Person/Invitee:
{
   "_id": "6bb06bde80925d1a058448ac4d006f6e",
   "_rev": "3-231654f6914afe2e20eb57a41ec8497a",
   "firstName": "Black",
   "lastName": "Francis",
   "type": "Person"
}
Checking one of the objects in the response in the debugger, I find that, indeed, the person is included in the list of results:


So far so good. This is exactly what I expect and my parse method should work fine. And it does. The problem is that the existing models in the collection look like:


That is, they look like:
attributes: Object
  id: "6bb06bde80925d1a058448ac4d006f6e"
That is just the model placeholder until the real thing can be fetched from the server. But, when the response is fetched from CouchDB, it comes back as:
doc: Object
  _id: "6bb06bde80925d1a058448ac4d006f6e"
  _rev: "3-231654f6914afe2e20eb57a41ec8497a"
  firstName: "Black"
  lastName: "Francis"
  type: "Person"
Do you see the problem there? I did not. Not for quite some time. The problem is that the existing placeholder has an "id" attribute, but the replacement from CouchDB has an "_id" attribute. CouchDB puts an underscore in front of the ID to indicate that it is meta data. Far be it for me to argue the wisdom of doing so, but it sure screws things up for me here.

The problem is that, when the model is fetched, it tries to replace the existing model, but there is no existing model. Although "id" and "_id" point to the same ID, they are two different attributes. And so Backbone simply adds the new model to the collection, retaining the placeholder. Hence the duplicates.

It took me a long time to track that down and to figure it out. As is usually the case, the solution is simple. In parse() I copy the "_id" attribute to "id":
    Invitees = Backbone.Collection.extend({
      // ...
      parse: function(response) {
        return _(response.rows).map(function(row) {
          var doc = row.doc;
          doc['id'] = doc['_id'];
          return doc;
        });
      }
    });
And with that, I have no more phantom invitees:


That was a pain to track down. It is more a function of using CouchDB than anything else, but I do wish I could have figured that out quicker.


Day #200

No comments:

Post a Comment