Minimal model tracking for Backbone.
Note: The development of this software is ongoing. A similar version has been used in production at Pathable for quite some time but your mileage may vary.
Post.has().many('comments', {
  collection: Comments,
  inverse: 'post'
});

Comment.has().one('post', {
  model: Post,
  inverse: 'comments'
});

var post = Post.create({
  id: 1,
  comments: [{id: 2}, {id: 3}, {id: 4}]
});

post.comments().length; // 3
var comment = Comment.create({id: 5, post_id: 1});
post.comments().length; // 4
comment.post() === post; // true :D
Source

The source for Supermodel is hosted on github. Please give it a read before trying anything serious as it will greatly improve the utility you gain from the library. The annotated source is also provided for easier reading. Supermodel is available for use under the MIT software license.

Supermodel depends on Backbone >= 0.9.2 and Underscore >= 1.3.3.

Change Log

0.0.4

  • Model.parent - Supermodel will no longer use __super__ to infer model relationships. Instead, they should be declared with Model.parent

Model

Supermodel.Model is an extension of Backbone.Model that handles the tracking and creation of individual models.

In large applications there are often multiple model objects representing the same server object. This can cause synchronization problems and cause the display of stale data. For instance:

var user = new User({id: 5, admin: true});
// ...
var duplicate = new User({id: 5, admin: false});
user.get('admin'); // true :(

If the server is updated while these models are being fetched the two instances may have conflicting attributes. To circumvent this, Supermodel tracks models in the all property of the constructor (which is itself a collection) and returns existing models instead of creating new ones when possible.

var user = User.create({id: 5, admin: true});
var duplicate = User.create({id: 5, admin: false});
// ...
duplicate === user; // true
user.get('admin'); // false :D
Model.create

In order to track models, Supermodel needs to check for their existence before returning a new model. This is the job of Model.create.

var User = Supermodel.Model.extend();
var user = User.create();

When using an existing model, it's assumed that the attributes provided are newer than the existing attributes and they are updated.

var user = User.create({id: 5, name: 'bradley'});
var duplicate = User.create({id: 5, name: 'brad'});
user.get('name'); // brad

To declare a collection for a Supermodel.Model you'll need to use a factory function rather than simply setting the model. This is so that collections can also use Model.create and benefit from tracking.

var Users = Backbone.Collection.extend({

  model: function(attrs, options) {
    return User.create(attrs, options);
  }

});

Note: You must use a factory function that calls create with the model constructor as context since Backbone.Collection will not do this for you.

Model.all

Each model is stored in a collection on the constructor for tracking and event propagation. This collection can be retrieved by calling Model.all(). These are rather handy for tracking all events for models of a given type.

var User = Supermodel.Model.extend();
var user = User.create({id: 3});
User.all().get(3) === user; // true

These also work for child constructors.

var Admin = User.extend();
var admin = Admin.create({id: 2});
Admin.all().get(2) === admin; // true
User.all().get(2) === admin; // true

A few things to keep in mind about inheritance with all collections:

  • All models with a common ancestor in their prototype chain (excluding Supermodel.Model) are assumed to have unique ids.
  • Models should always be created with the most specialized constructor possible. A model's super model can be deduced from the prototype chain, but its sub model cannot.

Whenever possible Supermodel attempts to prevent duplicate models but it's still possible to corrupt the all collection. For instance, creating a new model without an id and then setting id to an existing models value will cause problems. However, this is no different than regular everyday backbone.

var user = User.create({id: 5});
var impostor = User.create();
impostor.set({id: 5});
User.all().get(5) === impostor; // true

It's generally best to not set ids explicitly but only rely on server data for this.

Note: all collections are not intended to be modified. Doing so is not supported and can have negative consequences for model tracking.

Model.reset

Supermodel.Model.reset is used to remove associations and tracked models so they can be garbage collected. This is most useful for testing and is not generally needed in application code.

var user = User.create({id: 3});
User.reset();
User.all().get(3); // null
Model.parent

The parent constructor, used to indicate that Supermodel should track inherited instances. Constructors are otherwise treated as stand alone models, regardless of prototype chain.

var Admin = User.extend({}, {
  parent: User
});
Model.prototoype.parse

In order to prevent conflicts between associations and their corresponding attributes, nested data is used and removed with model.parse. Parsing is also done on new models during initialization.

Nested data can be provided through the source property in a model's attributes.

var post = new Post({
  id: 1,
  comments: [
    {id: 2, body: '…'},
    {id: 3, body: '…'}
  ]
});

Associations can also be specified through id properties.

var comment = new Comment({
  id: 4,
  body: '…',
  post_id: 1
});
Associations

When initializing models or changing their attributes, there is often plenty of information to wire up associations between models. For instance, in the following example it's clear that the membership belongs to the user and vice versa.

var user = new User({id: 5});
var membership = new Membership({id: 2, user_id: 5});

However, these things must be set manually in practice.

membership.user === user; // false :(
user.memberships.contains(membership); // false :(

The problem is finding the correct model and ensuring it's the canonical representation of that model. Supermodel.Model already handles these things for us so all that's left is to wire up specific associations.

User.has().many('memberships', {
  collection: Memberships,
  inverse: 'user'
});

Membership.has().one('user', {
  model: User,
  inverse: 'memberships'
});

var user = User.create({id: 5});
var membership = Membership.create({id: 2, user_id: 5});

membership.user() === user; // true \o/
user.memberships().contains(membership); // true \o/

When models are created or changed, they're inspected for appropriate attributes and the associated properties are set. When a model becomes associated or dissociated with another model an 'associate:<name>' or 'dissociate:<name>' event is triggered, respectively.

Association properties are retrieved using a getter function. This allows optimizations such as lazy-loading through collections. Other associations may be lazy-loaded in the future as well.

has

Associations are specified using Model.has(). This prevents naming collisions and provides a convenient extension point outside of Model itself.

owner

A reference to the model that owns an associated collection. This is useful when specifying a collection url that's relative to its owner.

Post.has().many('comments', {
  inverse: 'post',
  collection: Comments.extend({
    url: function() {
      return '/posts/' + this.owner.id + '/comments';
    }
  })
});
one

Constructor.has().one(name, options)

Instances of Constructor should contain a reference to one model, stored in the property specified by store and retrieved using the function specified by name.

User.has().one('settings', {
  model: Settings,
  inverse: 'user'
});

Settings.has().one('user', {
  model: User,
  inverse: 'settings'
});

var user = User.create({id: 2});
var settings = Settings.create({user_id: 2});

settings.user() === user; // true
user.settings() === settings; // true
Options
  • model - The constructor to use when creating the associated model.
  • inverse - The name of the inverse association, for notifying the associated model of 'associate' and 'dissociate' events.
  • id - The attribute where the id of the associated model is stored.
  • source - The attribute where the associated model's attributes are stored.
  • store - The property where the model should be stored. Defaults to '_' + name.
many

Constructor.has().many(name, options)

Instances of Constructor should contain a collection with many models, retrieved with a function stored at store and retrieved with the function stored at name.

User.has().many('memberships', {
  collection: Memberships,
  inverse: 'user'
});

Membership.has().one('user', {
  model: User,
  inverse: 'memberships'
});

var user = User.create({id: 5});
var membership = Membership.create({id: 2, user_id: 5});

membership.user() === user; // true :D
user.memberships().contains(membership); // true :D
options
  • collection - The constructor to use when creating the associated collection.
  • inverse - The name of the inverse association, for notifying the associated model of 'associate' and 'dissociate' events.
  • source - The attribute where the associated models' attributes are stored.
  • store - The property where the collection should be stored. Defaults to '_' + name.
  • through - The name of the through collection.
through

The functionality of many is changed significantly when a through option is specified. It's intended to track many-to-many associations through other collections. For example:

User.has()
  .many('memberships', {
    collection: Memberships,
    inverse: 'user'
  })
  .many('groups', {
    source: 'group',
    collection: Groups,
    through: 'memberships'
  });

Membership.has()
  .one('user', {
    model: User,
    inverse: 'memberships'
  })
  .one('group', {
    model: Group,
    inverse: 'memberships'
  });

Group.has()
  .many('memberships', {
    collection: Memberships,
    inverse: 'group'
  })
  .many('users', {
    source: 'user',
    collection: Users,
    through: 'memberships'
  });

var user = User.create({id: 3});
var group = Group.create({id: 6});
var membership = Membership.create({user_id: 3, group_id: 6});

membership.user() === user; // true
membership.group() === user; // true

user.memberships().contains(membership); // true
group.memberships().contains(membership); // true

user.groups().contains(group); // true
group.users().contains(user); // true