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/

8 comments:

  1. Interesting solution. Any chance you could post sample json for A & B, for GET/POST/PUSH requests and responses? Thanks!!!

    ReplyDelete
  2. Unfortunately, I can't seem to get this to work. Is the relation in abJoins supposed to be a HasMany?? Yeah, a complete working example would be great.

    ReplyDelete
  3. Paul, can you give me details on what problem you're having? I can try to get a more detailed example sometime this week, in the meantime I might be able to help you

    ReplyDelete
  4. Hi Mike, first off wondering if the relation in abJoins is supposed to be a HasMany not a HasOne. Also, how are you sending data up to the backbone app? A fetch? If so what the json looks like. That would be a great help. Thanks!

    ReplyDelete
  5. Paul, I've updated to post to include an executable example, one thing that might be causing problems is passing the `{parse:true}` option to your constructor? Hopefully the fiddle helps! Let me know if it was something else so i can share that in the blog too!
    http://jsfiddle.net/mmacaula/XaESG/2/

    ReplyDelete
  6. I was never quite comfortable with Backbone-relational not supporting many-to-many relations out of the box, so the result was to write an own implementation of relations between models, using some of the concepts of Backbone-relational, however supporting many-to-many relations without a linking model in between. We made the repository public yesterday, maybe you would like to check it out some time:

    https://github.com/jj-studio/Backbone-JJRelational/

    ReplyDelete
  7. Hey Jonathan! Why just not adding bb-relational support for many to many?

    ReplyDelete
    Replies
    1. Hey, I guess you are the same person as in this similiar issue on github. So for anyone else wondering, here's the link to the issue: https://github.com/jj-studio/Backbone-JJRelational/issues/1

      Delete