Tuesday, November 8, 2011

Replacing Homespun "Has Many" with Backbone-Relational

‹prev | My Chain | next›

I got started with backbone-relational last night. It took me a bit, but I ended up making some progress. Tonight, I hope to actually get it working in my Backbone.js calendar application.

The specific use case for which I have need of backbone-relational are the calendar appointments in my application. Calendar appointments have many people invited to them:


In JSON, the invitees attribute of this appointment is simply a list of IDs:
{
   "_id": "6bb06bde80925d1a058448ac4d004fb9",
   "_rev": "2-7fb2e6109fa93284c19696dc89753102",
   "title": "Test #7",
   "description": "asdf",
   "startDate": "2011-11-17",
   "invitees": [
       "6bb06bde80925d1a058448ac4d0062d6",
       "6bb06bde80925d1a058448ac4d006758",
       "6bb06bde80925d1a058448ac4d006f6e"
   ]
}
And, to get that invitees attribute loading actual Invitee models, I had to add a relations attribute to my Appointment relational-model:
    Appointment = Backbone.RelationalModel.extend({
      // ...
      relations: [
        {
          type: Backbone.HasMany,
          key: 'invitees',
          relatedModel: 'Invitee',
          collectionType: 'Invitees'
        }
      ],
      // ...
    });
At this point, I can load the invitees in the Javascript console, but they are no longer showing up in appointment dialog. For that, I need to replace the loadInvitees method call from my pre-backbone-relational days with the fetchRelated() method from backbone-relational:
    Appointment = Backbone.RelationalModel.extend({
      // ...
      initialize: function(attributes) {
        if (!this.id)
          this.id = attributes['_id'];

        this.fetchRelated("invitees");
        // this.loadInvitees();
      },
      // ...
    });
With that change, I have an invitees collection in the "invitees" attribute of my model. To make use of that, I pass said collection to the collection view (but only if people have been invited):
    var AppointmentEdit = new (Backbone.View.extend({
      // ...
      showInvitees: function() {
        $('.invitees', this.el).remove();
        $('#edit-dialog').append('<div class="invitees"></div>');

        if (this.model.get("invitees").length == 0) return this;

        var view = new Invitees({collection: this.model.get("invitees")});

        $('.invitees').append(view.render().el);

        return this;
      }
    }));
Amazingly, that works! It turns out that I was not that far away from success last night after all.

Although it does work—the invitees again show up in the appointment dialog—this works because of the somewhat hackish collection fetch() model that I wrote a few nights back. Specifically, the collection overrides fetch to individually retrieve the models specified by the list of IDs, manually triggering the "reset" event when complete.

Looking through the backbone-relational documentation, I see that it recommends mucking with the collection's URL so that it can request multiple IDs from the backend.

First up, my backend. Since I am using CouchDB, I can POST from my node.js backend to CouchDB, requesting all documents with the IDs POSTed in the request body:
app.get('/invitees', function(req, res){
  var options = {
    method: 'POST',
    host: 'localhost',
    port: 5984,
    path: '/calendar/_all_docs?include_docs=true'
  };

  var couch_req = http.request(options, function(couch_response) {
    console.log("Got response: %s %s:%d%s", couch_response.statusCode, options.host, options.port, options.path);

    couch_response.pipe(res);
  }).on('error', function(e) { /* ... */ });

  var ids = req.param('ids').split(/,/);
  couch_req.write(JSON.stringify({"keys":ids}));
  couch_req.end();
});
Admittedly, that is somewhat exotic, but it works. Now, I need to be able to GET the /appointments resource with a query parameter of a comma separated list of strings.

As suggested by the backbone-relational documentation, I do that in the url() method of my collection (url can be either property or method in Backbone):
    Invitees = Backbone.Collection.extend({
      model: Models.Invitee,

      url: function( models ) {
        return '/invitees' + ( models ? '?ids=' + _.pluck( models, 'id' ).join(',') : '' );
      },
      parse: function(response) {
        return _(response.rows).map(function(row) { return row.doc;});
      }
    });
(I also have to parse() the results coming back from CouchDB to ensure that they map into an array of Model attributes).

And (again) amazing, this works. Except... that my collections have now doubled in size with the first bunch of elements being empty:


I do a little bit of digging, but am unable to see an obvious explanation for the phantom invitees. I will pick back up here tomorrow to solve this minor mystery (and hopefully conclude my exploration of backbone-relational).

I am still undecided on backbone-relational at this point. It has certainly guided me to a cleaner solution. But the reliance on global variable definitions for the models and collections remains worrisome. Perhaps another day's exploration will ease my concerns.


Day #199

2 comments:

  1. Very curious to see what you end up deciding. I'm going to try out Backbone-Relational sooner or later, but I'm of two minds about this whole category of project. I've seen two Backbone projects already which you could kind of characterize as half-baked reimplementations of ActiveRecord from Rails.

    I think Backbone's Collections exist partly because some projects will require objects to know more about their associations than others. They're pretty lightweight, in a sense, and I think that's a feature rather than a bug. Extending them (and Models) to handle associations seems an inevitability but I'm not sure what it will look like when we finally see that handled gracefully. (I'm hoping it's Backbone-Relational!)

    Anyway, fan of the blog, so I'll dig into some of your other posts on this topic.

    ReplyDelete
  2. @Giles Thanks! I ended up being entirely non-committal about the whole matter. I hit corner cases relatively quickly that backbone-relational didn't quite handle. The underlying code in backbone-relational is nowhere near as clean as in Backbone.js proper, which made handling those corners difficult.

    In the end, I think I am more comfortable rolling my own model-has-a-collection rather than using backbone-relational. Still backbone-relational isn't so horrible that I'd force a client off of it.

    ReplyDelete