javascriptbackbone.jsbackbone-relationalknockback.js

Implementing a Many-to-Many relationship with Backbone-Relational


I have a simple application which defines two classes, a Person and a PersonGroup, wherein there is a many-to-many relationship in place. A Person can have no group, or be assigned to all groups, and anything in between.

The example on backbonerelational.org suggests using an in-between model for many-to-many relationships, however I can't get this pattern to work with fetching (deserializing) and saving (serializing).

What I want to do is use Backbone to deserialize a JSON similar to the following:

{
    People:
    [
        {
            "ID": 1,
            "Name": "John"
        },
        {
            "ID": 2,
            "Name": "Bob"
        },
        {
            "ID": 3,
            "Name": "Tim"
        },
    ],
    PeopleGroups:
    [
        {
            "ID": 1,
            "Name": "Owners",
            "People":
            [
                1,
                2
            ],
        },
        {
            "ID": 2,
            "Name": "Everyone",
            "People":
            [
                1,
                2,
                3
            ],
        },
    ]
}

I'm using Knockback/Knockout for data binding so the problem is I need to be able to access the relationships by reference. An array of IDs does not do me any good, unless I can create a Knockback.CollectionObservable to wrap the collection and resolve the IDs to references.


Solution

  • I ended up getting this to work the way I wanted to. The Many-to-Many relationship is maintained in the database but the relationships can only be accessed in my Backbone models in one direction. I decided that the Person models would be stand-alone and the PersonGroup models would have a collection of references to the Person models they are linked to.

    The key points here that made everything work were to specify includeInJSON: "ID" and to remove the reverseRelation. This still lets you access the references to the models in JavaScript, but it correctly serializes and deserializes to JSON. The Person models simply don't have access to a navigation property to the Groups they are in, however they can exist in multiple groups just fine.

    I simply assumed that using a list of IDs would mandate jumping through hoops to resolve references, but Backbone-relational seems to use the global Backbone model store to resolve references by ID without creating duplicate models. (eg. Three different groups can reference the same Person and only one model is ever created).

    var Person = Backbone.RelationalModel.extend(
    {
        idAttribute: "ID",
    });
    
    var PersonGroup = Backbone.RelationalModel.extend(
    {
        idAttribute: "ID",
    
        relations:
        [
            {
                type: Backbone.HasMany,
                key: "People",
                relatedModel: "Person",
                collectionType: "PersonCollection",
                includeInJSON: "ID",
            },
        ],
    });
    

    And here's the rest of the plumbing if it helps anyone.You can define the Backbone.Collections as follows and obtain them through separate API requests:

    var PersonCollection = Backbone.Collection.extend(
    {
        model: Person,
    
        url: "/api/person",
    });
    
    var PersonGroupCollection = Backbone.Collection.extend(
    {
        model: PersonGroup,
    
        url: "/api/persongroup",
    });
    
    var PersonModels = new PersonCollection();
    
    var GroupsModels = new PersonGroupCollection();
    
    this.PersonModels.fetch();
    
    this.GroupsModels.fetch();
    
    this.People = kb.collectionObservable(
        this.PersonModels,
        {
            factories:
            {
                "models": PersonViewModel,
            },
        }
    );
    
    this.PersonGroups = kb.collectionObservable(
        this.GroupsModels,
        {
            factories:
            {
                "models": PersonGroupViewModel,
                "models.People.models": PersonViewModel,
            },
        }
    );
    

    I included the Knockback.js specific collections for using Knockout.js bindings. Only one ViewModel is created per model so change tracking is propagated through the entire app.