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
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.
0.0.4
Model.parent - Supermodel will no longer use
__super__
to infer model relationships. Instead, they should be declared withModel.parent
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
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.
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.
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
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
});
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
});
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.
Associations are specified using Model.has()
. This prevents naming collisions and provides a convenient extension point outside of Model
itself.
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';
}
})
});
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
- 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.
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
- 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.
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