3

I've been over the docs quite a few times, but this aspect still isn't clear to me. It's entirely possible that I'm thinking backbone-relational does something that it doesn't.

I'm looking for the way to define relationships based on key to avoid all the boilerplate fetching nonsense.

Take the canonical Artists and Albums example: An artist has many albums as defined by album.artist_id

/api/artist/62351 might return

{
  id: 62351,
  name:"Jimi Hendrix"
}

similarly /api/album?artist_id=62351 might return

[
 {
   id:5678,
   name: "Are You Experienced?"
   artist_id: 62351
 },
 {
   id: 4321,
   name: "Axis: Bold as love",
   artist_id: 62351
 }
]

How might I define Artist and Album relationships such that

var hendrixInstance = new Artist({id:62351});
hendrixInstance.get('albums');

would fetch and return a collection of albums based on the album foreign_key artist_id? It must just be some permutation of key/keySource/keyDestination that I've yet to try, or be a problem that backbone-relational isn't trying to solve, but my doc groking has failed and I think a concise answer to this on SO might help future Googlers.

var Artist = Backbone.RelationalModel.extend({
  urlRoot: '/api/artist',
  relations:[{
    key: 'albums', //Docs say this is the foreign key name, but in practice it doesn't appear that way. Need keySource/Destination?
    type: Backbone.HasMany,
    reverseRelation: {
      key: 'artist',
      type: Backbone.HasOne
    }
  }]
});
var Album = Backbone.RelationalModel.extend({
  urlRoot: '/api/album'
});

Bonus points to an example model that references its self adjacency list style with parent_id

nebulous
  • 738
  • 6
  • 17

2 Answers2

3

So, @xzhang's method above kept me iterating on this problem. First off, I'd love to be proven wrong on this, but I haven't found a way that backbone-relational handles this problem without additional custom code. Since this in my mind is an incredibly basic example of a OneToMany relationship, I'm still holding out hope that I'm just not getting something obvious.

Here's what I ended up doing to handle the situation. Unfortunately it still does not automatically fetch from the server when someobject.fetch('somerelationship') is called, which is what I really want. The parse function won't be necessary for most people, but it's required for the api I'm calling.

First I set up a base collection from which to extend:

  var BaseCollection = Backbone.Collection.extend({
    initialize: function(models, options) {
      if (_.isObject(options.relation)) {
      this.url = '/api/'
        + options.relation.keySource
        + '?search.'+options.relation.reverseRelation.keySource
        + '=' + options.foreignId;
      }
    },
    parse: function(res) { return res.success ? res.list : res },
  });

Then a reusable helper function (could probably be rolled into BaseCollection) to assist with creating relationships

  function collectionOptions(instance) {
    return {"relation":this, "foreignId":instance.get('id') };
  };

And finally, those relationships are told to use BaseCollection as their CollectionType, and the collectionOptions() helper is assigned to set collectionOptions.

 var Form = BaseModel.extend({
    urlRoot: '/api/form',
    relations:[
      {
        key: 'fills',
        keySource: 'fill',
        relatedModel: Fill,
        type: Backbone.HasMany,
        collectionOptions: collectionOptions,
        collectionType: BaseCollection,
        reverseRelation: {
          key: 'form',
          keySource: 'form_id',
          type: Backbone.HasOne
        }
      },{
        key: 'children',
        keySource: 'form',
        relatedModel: 'Form',
        type: Backbone.HasMany,
        collectionOptions: collectionOptions,
        collectionType: BaseCollection,
        reverseRelation: {
          key: 'parent',
          keySource: 'parent_id',
          type: Backbone.HasOne
        }
      }
    ]
  });

This allows me to avoid changing the server side API to return a list of ids and then individually fetch those ids. Instead I can just:

var form = new Form({id:1});
form.get('children').fetch();
form.toJSON(); //now has {id:1, ..., ..., children:[child,child,child,...]}

An extension to autoFetch children on the first call to .get('children') would be just the ticket, but I haven't discovered how to do that without modifying backbone-relational itself.

nebulous
  • 738
  • 6
  • 17
0

I am facing the exactly problem (Backbone-relational hasmany best practices), after 2 days research and look into the source code, I don't think key/keySource/keyDestination will do the work (correct me if I am wrong).

So I end up with create my own relation type, so far works fine. This may not a good solution, but hope can help you.

var LazyMany = Backbone.HasMany.extend({
  setRelated: function (related) {
    var relation = this.options
      , instance = this.instance
    ;

    if (related && !_.result(related, 'url')) {
      related.url = relation.relatedModel.prototype.urlRoot + 
        '?' + relation.reverseRelation.key + '=' + instance.id;
    }
    return LazyMany.__super__.setRelated.apply(this, arguments);
  }
});

Then in your model:

var Album = Backbone.RelationalModel.extend({
  urlRoot: '/api/album/'
});

var Artist = Backbone.RelationalModel.extend({
  urlRoot: '/api/artist/',
  relations:[{
    key: 'albums',
    type: LazyMany,
    includeInJSON: false,
    relatedModel: Album,
    reverseRelation: {
      key: 'artist',
      // I didn't test this, I don't have artist_id, artist is "id" in my app
  keySource: 'artist_id',
  keyDestination: 'artist_id',
      includeInJSON: 'id'
    }
  }]
});

So if you don't define a collectionType or your collection don't have a url field, LazyMany will create a collection with url: /api/album/?artist=62351. Then you just need fetch the collection: artist.get('albums').fetch().

Hope this can help, and I am still looking for better solutions.

Community
  • 1
  • 1
xzhang
  • 563
  • 6
  • 18
  • Thanks @xzhang, I'm glad to see I'm not the only one who expected something different out of backbone-relational. I tried your method and it does indeed work, but after several additional iterations I ended up using a variation. – nebulous Apr 16 '13 at 14:08