Sunday, July 7, 2013

Setting up Many to Many relationships in Backbone Relational

We're using Backbone Relational in our application.  We actually switched to relational after trying to manage those relationships ourselves via parse and initialize methods but we found that to be really annoying and frustrating.  Relational has some nice benefits, automatically fetching relationships, keeping a store of objects and enforcing relationships.  One thing it doesn't do though is Many to Many relationships.  I'd like to share how we went about doing that in case other people would find it useful.

The path forward is recommended on relational's documentation, and a classic way of handling many-to-many, try to split it into two one-to-many relationships.  The intent is that your domain model really should be split up into two relationships, but sometimes you still need to have that many-to-many.

The best way to deal with this without re-writing Relational or using another library (none of which currently do Many To Many relationships) is to break up the Many To Many into two One To Many relationships.

Our Example.

Let's keep it simple, A has many B's (plural of 'b', pronounced 'bees'), B has many A's.


Normally you can only setup one of those relationships, and Relational will not allow you to make another.  If you try it anyway, or don't realize you even have this type of relationship, Relational will actually remove the relationship from the first model that gets it and set it up on the other one.  This can cause annoying errors where things are null when they shouldn't be. 

If you want to keep it many to many, you need to set up a "join" object, in fact this is doing exactly what docs would say to do, but the docs suggest you have some sort of concept for this join table, and if you do have a concept for this, then by all means do so.  


The Implementation


First off, pick a parent relationship, we'll use A -> B in this case.  Then for every A model, you construct a join object AB.  That join object then has a hasMany object to to B.  

So, let's say we have our definition for B, it is pretty basic, just a regular model:

var models = {}
models.ModelB = Backbone.RelationalModel.extend({
    urlRoot: 'someUrl'
});
models.CollectionB = Backbone.Collection.extend({
    model: models.ModelB
});
Next, we need to build our 'Join' model and collection
// Join table
models.ABJoin = Backbone.RelationalModel.extend({
    relations: [
        {
            type: Backbone.HasOne,
            key: 'b',
            relatedModel: models.ModelB,
            reverseRelation: {
                key: 'abJoins',
                includeInJSON: false
            }
        }
    ],

    toJSON: function () {
        return this.get('b').id;
    }
});

models.ABJoinCollection = Backbone.Collection.extend({
    model: models.ABJoin
});
This is where it starts to get more interesting. What we're defining here is a go-between of A to B. This object will have a 'b' reference AND an 'a' reference (we'll get to that later). Each join object will have many A's and many B's. We still want to be able to have Backbone handle saves, deletes, and posts correctly though. To that end, we override the toJSON() to return what we want directly from 'b'. This support's our model of returning the id. You could have it delegate to 'b's toJSON() method if you wish. Finally, we need to define our A model:
models.ModelA = Backbone.RelationalModel.extend({
    url: 'someUrl',
    relations: [
        {
            type: Backbone.HasMany,
            key: 'abJoins',
            relatedModel: models.ABJoin,
            collectionType: models.ABJoinCollection,
            keyDestination: 'bs',
            autoFetch: false,
            reverseRelation: {
                key: 'a',
                includeInJSON: true
            }
        }
    ],
    parse: function (response) {
        // plural of 'b'
        var bs = response.bs;
        if (bs) {
            response.abJoins = _.map(bs, function (b) {
                return {b: b};
            });
            delete response.bs;
        }

        return response;
    }
});

The relation defined above says that A will have many join objects, with the reverse relation (defaulting to one to many) being named 'a'. That's how the join object also has an 'a' reference (in addition to the 'b' reference). There are a couple of other things of note here. Notice the key destination 'bs'. This is also to support Backbone Sync, essentially telling Relational that when you toJSON the join table, call the result 'bs'. You may also notice the key is 'abJoins', but we don't want the server to be aware of this join object, in all likelihood the server is sending you an array of 'b' objects. That's where the overridden parse method comes in. Parse get's the raw JSON and allows us to tweak it. So what we do is take that JSON and wrap it (using underscore lib here) in a join object JSON. You may ask why the 'if' check there. Well, a weird thing with Relational we found was that parse would be called multiple times, so we had to be a bit defensive.

Edit:  One thing not to forget when passing your json to your models or collection is to set the {parse : true} option!

That's pretty much it.  We've been using this for a couple of months now.  It's nice in that it works, but it definitely doesn't feel completely right.  There's a lot of boilerplate that will need to go on for every relationship like this.  However until something better comes along, or data models stop having many-to-many relationships, it will do.

Edit:  I spent some time making an executable example on the above.  Here's the jsFiddle:  http://jsfiddle.net/mmacaula/XaESG/2/