When developing any medium to large-scale web application, you often get to the point where an action by a user can cause a number of different models to change on the client and the server.
You can try to keep updating both sides of a relation manually for every action, and individually call save() or fetch() on each of the changed models to sync with the server, but that quickly turns into a tedious process and results in multiple requests. Instead, we can configure relationships between our models, and sync the model and all of its related models with a single save() or fetch().
Backbone-relational is hosted on GibHub, and is available under the MIT license.
| Latest Release (0.8.0) | ~60kb, Full source, lots of comments |
| Development Version |
Backbone-relational depends on Backbone.js (> 0.9.10), which itself requires Underscore.js (> 1.4.4) and jQuery (> 1.4.2) or Zepto.
Backbone-relational.js provides one-to-one, one-to-many and many-to-one relations
between models for Backbone. To use relations, extend Backbone.RelationalModel
(instead of a regular Backbone.Model) and define a
relations
property, containing an array of option objects.
Each relation must define (at least) the type
, key
,
and relatedModel
. Available relation types are
Backbone.HasOne
and Backbone.HasMany
.
Backbone-relational's main features include:
includeInJSONoption.
createModelsoption.
fetchRelatedmethod.
collectionType.
You can also bind new events to a Backbone.RelationalModel for an:
Backbone-relational depends on Backbone.js (and thus on Underscore.js). Include Backbone-relational right after Backbone and Underscore:
<script type="text/javascript" src="./js/underscore.js"></script>
<script type="text/javascript" src="./js/backbone.js"></script>
<script type="text/javascript" src="./js/backbone-relational.js"></script>
When using Backbone-relational, each model defining (or receiving) relations
must extend
Backbone.RelationalModel in order to function. Backbone.RelationalModel
introduces a couple of new methods, events and properties. It's important to know which are properties,
which are methods of an instance, and which operate on the type itself.
These three subcategories are detailed below.
Properties can be defined when extending Backbone.RelationalModel, or a subclass thereof.
Instance methods operate on an instance of a type.
Static methods operate on the type itself, as opposed to operating on model instances.
relation[]
Each Backbone.RelationalModel can contain an array of relation definitions. Each relation supports a number of
options, of which relatedModel
, key
and
type
are mandatory. A relation could look like the following:
Zoo = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'animals',
relatedModel: 'Animal',
collectionType: 'AnimalCollection',
reverseRelation: {
key: 'livesIn',
includeInJSON: 'id'
// 'relatedModel' is automatically set to 'Zoo'; the 'relationType' to 'HasOne'.
}
}]
});
Animal = Backbone.RelationalModel.extend({
urlRoot: '/animal/'
});
AnimalCollection = Backbone.Collection.extend({
model: Animal
});
// We've now created a fully managed relation. When you add or remove model from `zoo.animals`,
// or update `animal.livesIn`, the other side of the relation will automatically be updated.
var artis = new Zoo( { name: 'Artis' } );
var lion = new Animal( { species: 'Lion', livesIn: artis } );
// `animals` in `artis` now contains `lion`
alert( artis.get( 'animals' ).pluck( 'species' ) );
var amersfoort = new Zoo( { name: 'Dierenpark Amersfoort', animals: [ lion ] } );
// `lion` now livesIn `amersfoort`, and `animals` in `artis` no longer contains `lion`
alert( lion.get( 'livesIn' ).get( 'name' ) + ', ' + artis.get( 'animals' ).length );
relation.key
Required. A string that references an attribute name on relatedModel
.
relation.relatedModel
Required. A string that can be resolved to an object on the global scope, or a reference to a
Backbone.RelationalModel. Also see addModelScope
.
relation.type
Required. A string that references a Backbone.Relation
type by name ("HasOne" or "HasMany"),
or a direct reference to a relation type.
You can model a one-to-one or a many-to-one relationship by declaring type
as the string "HasOne", or by
directly referencing Backbone.HasOne
. A HasOne relation contains a single
Backbone.RelationalModel. The default reverseRelation.type
for a "HasOne" relation is
"HasMany". This can be set to "HasOne" instead, to create a one-to-one relation.
You can model a one-to-many relationship by declaring type
as the string "HasMany", or by directly
referencing Backbone.HasMany
. A HasMany relation contains a Backbone.Collection,
containing zero or more Backbone.RelationalModels. The default reverseRelation.type
for a HasMany relation is HasOne; this is the only option here, since many-to-many is not supported directly.
It is possible model a many-to-many relationship using two Backbone.HasMany
relations, with a link model in between:
Person = Backbone.RelationalModel.extend({
relations: [{
type: 'HasMany',
key: 'jobs',
relatedModel: 'Job',
reverseRelation: {
key: 'person'
}
}]
});
// A link object between 'Person' and 'Company'
Job = Backbone.RelationalModel.extend({
defaults: {
'startDate': null,
'endDate': null
}
})
Company = Backbone.RelationalModel.extend({
relations: [{
type: 'HasMany',
key: 'employees',
relatedModel: 'Job',
reverseRelation: {
key: 'company'
}
}]
});
relation.includeInJSON
A boolean, a string referencing one of the model's attributes, or an array of strings referencing model
attributes. Default: true
.
Determines how the contents of a relation will be serialized following a call to the
toJSON
method. If you specify a:
Backbone.Model.prototype.idAttributeto include ids.
Specifying true
will cascade, meaning the relations of nested model will get serialized as well,
until either a different value is found for includeInJSON
or we encounter a model that has already
been serialized.
relation.autoFetch
A boolean or an object. Default: false
.
If this property is set to true
, when a model is instantiated the related model is
automatically fetched using fetchRelated
. The
value of the property can also be an object. In that case the object is passed to
fetchRelated
as the options parameter.
Note that autoFetch
operates independently from other `fetch` operations,
including those that may have fetched the current model.
var Shop = Backbone.RelationalModel.extend({
relations: [
{
type: Backbone.HasMany,
key: 'customers',
relatedModel: 'Customer',
autoFetch: true
},
{
type: Backbone.HasOne,
key: 'address',
relatedModel: 'Address',
autoFetch: {
success: function( model, response ) {
//...
},
error: function( model, response ) {
//...
}
}
}
]
});
relation.collectionType
A string that can be resolved to an object type on the global scope, or a reference to a Backbone.Collection type.
Determine the type of collections used for a HasMany relation. If you define a
url(models<Backbone.Model[]>) function on the specified collection, this enables
fetchRelated
to fetch all missing models in one request, instead of
firing a separate request for each.
relation.collectionKey
A string or a boolean. Default: true
.
Used to create a back reference from the Backbone.Collection used for a HasMany relation to the model on
the other side of this relation. By default, the relation's key attribute will be used to create a reference to
the RelationalModel instance from the generated collection. If you set collectionKey
to a string,
it will use that string as the reference to the RelationalModel, rather than the
relation's key attribute. If you don't want this behavior at all, set collectionKey
to false
(or any falsy value) and this reference will not be created.
relation.collectionOptions
An options hash, or a function that accepts an instance of a Backbone.RelationalModel and returns an options hash.
Used to provide options for the initialization of the collection in the 'Many'-end of a HasMany relation. Can be an options hash or a function that should take the instance in the 'One'-end of the 'HasMany' relation and return an options hash.
relation.createModels
A boolean. Default: true
.
Specifies whether models will be created from nested objects or not.
relation.keySource
A string that references an attribute to deserialize data for relatedModel
from.
Used to override key when determining what data to use when (de)serializing a relation, since the data backing
your relations may use different naming conventions. For example, a Rails backend may provide the keys suffixed
with _id
or _ids
. The behavior for keySource
corresponds to the following rules:
When a relation is instantiated, the contents of the keySource
are used as it's initial data. The
application uses the regular key attribute to interface with the relation and the models in it; the
keySource
is not available as an attribute for the model. So you may be provided with data containing
animal_ids
, while you want to access this relation as zoo.get('animals')
.
Note that setting keySource
will set keyDestination
to the same value, if it isn't specified itself.
This means that when saving zoo, the animals attribute will be serialized back into the animal_ids
key.
WARNING: when using a keySource, you should not use that attribute name for other purposes.
relation.keyDestination
A string that references an attribute to serialize relatedModel
into.
Used to override key (and keySource
) when determining what attribute to be
written into when serializing a relation, since the server backing your relations may use different naming
conventions. For example, a Rails backend may expect the keys to be suffixed with _attributes for nested
attributes.
When calling toJSON
on a model (either via
Backbone.Sync, or directly), the data in the key attribute is transformed and assigned to the
keyDestination
.
So you may want a relation to be serialized into the animals_attributes key, while you want to access this
relation as zoo.get( 'animals' );
.
WARNING: when using a keyDestination
, you should not use that attribute name for other purposes.
var FarmAnimal = Animal.extend();
// This `Farm` is confused, like legacy stuff can be. It wants its data back on a completely
// different key than it supplies it on. We want to use a different one in our app as well.
var Farm = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'animals',
keySource: 'livestock',
keyDestination: 'pets',
relatedModel: FarmAnimal,
reverseRelation: {
key: 'farm',
includeInJSON: 'name'
}
}]
});
// Create a `Farm`; parse `species`, add to `animals`, output goes to `pets`.
var farm = new Farm( { name: 'Old MacDonald', livestock: [ { species: 'Sheep' } ] } );
farm.get( 'animals' ).add( { species: 'Cow' } );
alert( JSON.stringify( farm.toJSON(), null, 4 ) );
relation.parse
A boolean. Default: false
.
If you have a relation where the models should be parsed when data is being set, specify `parse: true`.
relation.reverseRelation
An object specifying the relation pointing back to this model from relatedModel
.
If the relation should be bidirectional, specify the details for the reverse relation here. It's only mandatory
to supply a key
; relatedModel
is automatically
set. The default type for a reverseRelation
is HasMany for a
HasOne relation (which can be overridden to HasOne in order to create a one-to-one relation), and HasOne for a
HasMany relation. In this case, you cannot create a reverseRelation
with
type HasMany as well; please see Many-to-many relations on how to model these type of relations.
Note that if you define a relation (plus a reverseRelation) on a model, but don't actually create an instance
of that model, it is possible initializeRelations
will never get called, and the reverseRelation
will not be initialized. This can happen when extend
has been overridden, or redefined as in CoffeeScript.
See setup.
relationalModel.subModelTypes(attributes<object>, [options<object>])
An object. Default: {}
.
A mapping that defines what submodels exist for the model (the superModel) on which
subModelTypes
is defined. The keys are used to match the
subModelTypeAttribute
when deserializing, and the values
determine what type of submodel should be created for a key. When building model instances from data, we need to
determine what kind of object we're dealing with in order to create instances of the right subModel type. This
is done by finding the model for which the key is equal to the value of the
subModelTypeAttribute
attribute on the passed in data.
Each subModel is considered to be a proper submodel of its superclass (the model type you're extending), with a shared id pool. This means that when looking for an object of the supermodel's type, objects of a submodel's type can be returned as well, as long as the id matches. In effect, any relations pointing to the supermodel will look for instances of it's submodels as well.
Mammal = Animal.extend({
subModelTypes: {
'primate': 'Primate',
'carnivore': 'Carnivore'
}
});
Primate = Mammal.extend();
Carnivore = Mammal.extend();
MammalCollection = AnimalCollection.extend({
model: Mammal
});
// Create a collection that contains a 'Primate' and a 'Carnivore'.
var mammals = new MammalCollection([
{ id: 3, species: 'chimp', type: 'primate' },
{ id: 5, species: 'panther', type: 'carnivore' }
]);
var chimp = mammals.get( 3 );
alert( 'chimp is an animal? ' + ( chimp instanceof Animal ) + '\n' +
'chimp is a carnivore? ' + ( chimp instanceof Carnivore ) + '\n' +
'chimp is a primate? ' + ( chimp instanceof Primate ) );
Suppose that we have an Mammal model and a Primate model extending Mammal. If we have a Primate object with id 3, this object will be returned when we have a relation pointing to a Mammal with id 3, as Primate is regarded a specific kind of Mammal; it's just a Mammal with possibly some primate-specific properties or methods.
Note that this means that there cannot be any overlap in ids between instances of Mammal and Primate, as the Primate with id 3 will be the Mammal with id 3.
relationalModel.subModelTypeAttribute
A string. Default: type
.
The subModelTypeAttribute
is a references an attribute on the data
used to instantiate relatedModel
. The attribute that will be checked to
determine the type of model that should be built when a raw object of attributes is set as the related value,
and if the relatedModel
has one or more submodels.
relationModel.getRelation(name<string>)
Returns: Backbone.Relation
A single initialized relation on the model.
relationModel.getRelations()
Returns: Backbone.Relation[]
The set of initialized relations on the model.
relationalModel.fetchRelated(key<string>, [options<object>], [update<boolean>])
Returns: deferred[]
An array of request objects.
Fetch models from the server that were referenced in the model's attributes, but have not been found/created yet. This can be used specifically for lazy-loading scenarios. Setting update to true guarantees that the model will be fetched from the server and any model that already exists in the store will be updated with the retrieved data. The options object specifies options to be passed to Backbone.Sync.
By default, a separate request will be fired for each additional model that is to be fetched from the server.
However, if your server/API supports it, you can fetch the set of models in one request by specifying a
collectionType for the relation you call fetchRelated on. The collectionType
should have an overridden url
method that allows it to construct a url for an array of models. See this example
or Backbone-tastypie for an example.
set(key<string>, value, [options<object>]) or set(attributes<object>, [options<object>])
Returns: Backbone.RelationalModel
The model instance.
The set
method is overridden so that setting a value on an "relational" attribute will update that relation.
This is especially important to keep in mind for HasMany
relations (which are backed by a Backbone.Collection
).
For these, calling set
can be thought of as being equivalent to calling update
on the collection itself,
including how the options are handled.
Additional options
for a HasMany
relation:
add
true. If true, models specified in the arguments but not yet present in the relation will be added to the relation.
merge
true. If true, existing models will be updated with the given attributes.
remove
true. If true, models present in the relation but not specified in the arguments will be removed.
relationModel.toJSON(name<string>)
Returns: Object
The JSON representation of the model.
See Backbone.Model.toJSON.
The regular toJSON
function has been overridden and modified to serialize (nested) relations
according to their includeInJSON
, keySource
,
and keyDestination
options.
relationModel.setup()
Returns: Backbone.RelationalModel.constuctor
The type.
Initialize the relations and submodels for the model type. Normally, this happens automatically, but it doesn't if
you're using CoffeeScript and using the syntax class MyModel extends Backbone.RelationalModel
instead of
the JavaScript equivalent of MyModel = Backbone.RelationalModel.extend()
.
This has advantages in CoffeeScript, but it also means that Backbone.Model.extend
will not get called.
Instead, CoffeeScript generates piece of code that would normally achieve the same. However, extend
is also
the method that Backbone-relational overrides to set up relations as you're defining your Backbone.RelationalModel
subclass.
In this case, you should call setup
manually after defining your subclass CoffeeScript-style. For example:
Note: this is a static method. It operate on the model type itself, not on an instance.
class MyModel extends Backbone.RelationalModel
relations: [
// etc
]
MyModel.setup()
relationalModel.build(attributes<object>, [options<object>])
Returns: Backbone.RelationalModel
A model instance.
Create an instance of a model, taking into account what submodels have been defined.
Note: this is a static method. It operate on the model type itself, not on an instance.
relationalModel.findOrCreate(attributes<string|number|object>, [options<object>])
Returns: Backbone.RelationalModel
A model instance.
Search for a model instance in the Backbone.Relational.store, and return the model if found.
If no model is found, a new model will be created if options.create
is true
.
Accepted options
:
create
true. If true, a new model will be created if an instance matching
attributesisn't found in the store.
merge
true. If true, a found model will be updated with
attributes(if
attributesis an
object).
parse
false. If true,
attributeswill be parsed first. Please note this will cause
Model.parseto be called as a function (
thiswill not point to a model), instead of as a method.
Note: this is a static method. It operate on the model type itself, not on an instance.
Backbone-relational makes a couple of additional events available to you, on top of the events already found in Backbone.
add:<key>→
function(addedModel<Backbone.Model>, related<Backbone.Collection>)
remove:<key>→
function(removedModel<Backbone.Model>, related<Backbone.Collection>)
change:<key>→
function(model<Backbone.Model>, related<Backbone.Model|Backbone.Collection>)
Each relation definition on a model is used to create in instance of a Backbone.Relation
; either
a Backbone.HasOne
or a Backbone.HasMany
.
Defines a HasOne relation. When defining a reverseRelation, the default type will be HasMany. However, this can also be set to HasOne to define a one-to-one relation.
Defines a HasMany relation. When defining a reverseRelation, the type will be HasOne.
Backbone.Store is a global model cache. Per application, one instance is created (much like Backbone.History
),
which is accessible as Backbone.Relational.store
.
Backbone.Relational.store.addModelScope( scope<object> )
When working in an environment without a shared global scope (like window
is in a browser), you'll need
to tell the store
where your models are defined, so it can resolve them to create and maintain relations.
Backbone.Relational.store.reset()
Reset the store
to it's original state. This will disable relations for all models created up to this point,
remove added model scopes, and removed all internal store collections.
A tutorial by antoviaque, and the accompanying git repository.
A basic working example to get you started:
var paul = new Person({
id: 'person-1',
name: 'Paul',
user: { id: 'user-1', login: 'dude', email: 'me@gmail.com' }
});
// A User object is automatically created from the JSON; so 'login' returns 'dude'.
paul.get('user').get('login');
var ourHouse = new House({
id: 'house-1',
location: 'in the middle of the street',
occupants: ['person-1', 'person-2', 'person-5']
});
// 'ourHouse.occupants' is turned into a Backbone.Collection of Persons.
// The first person in 'ourHouse.occupants' will point to 'paul'.
ourHouse.get('occupants').at(0); // === paul
// If a collection is created from a HasMany relation, it contains a reference
// back to the originator of the relation
ourHouse.get('occupants').livesIn; // === ourHouse
// The `occupants` relation on 'House' has been defined as a HasMany, with a reverse relation
// to `livesIn` on 'Person'. So, 'paul.livesIn' will automatically point back to 'ourHouse'.
paul.get('livesIn'); // === ourHouse
// You can control which relations get serialized to JSON, using the 'includeInJSON'
// property on a Relation. Also, each object will only get serialized once to prevent loops.
alert( JSON.stringify( paul.get('user').toJSON(), null, '\t' ) );
// Load occupants 'person-2' and 'person-5', which don't exist yet, from the server
ourHouse.fetchRelated( 'occupants' );
// Use the `add` and `remove` events to listen for additions/removals on a HasMany relation.
// Here, we listen for changes to `ourHouse.occupants`.
ourHouse
.on( 'add:occupants', function( model, coll ) {
console.log( 'add %o', model );
// Do something. Create a View?
})
.on( 'remove:occupants', function( model, coll ) {
console.log( 'remove %o', model );
// Do somehting. Destroy a View?
});
// Use the 'update' event to listen for changes on a HasOne relation (like 'Person.livesIn').
paul.on( 'change:livesIn', function( model, attr ) {
console.log( 'change `livesIn` to %o', attr );
});
// Modifying either side of a bi-directional relation updates the other side automatically.
// Take `paul` out or `ourHouse`; this triggers `remove:occupants` on `ourHouse`,
// and `change:livesIn` on `paul`
ourHouse.get( 'occupants' ).remove( paul );
alert( 'paul.livesIn=' + paul.get( 'livesIn' ) );
// Move into `theirHouse`; triggers 'add:occupants' on ourHouse, and 'change:livesIn' on paul
theirHouse = new House( { id: 'house-2' } );
paul.set( { 'livesIn': theirHouse } );
alert( 'theirHouse.occupants=' + theirHouse.get( 'occupants' ).pluck( 'name' ) );
This is achieved using the following relations and models:
House = Backbone.RelationalModel.extend({
// The 'relations' property, on the House's prototype. Initialized separately for each
// instance of House. Each relation must define (as a minimum) the 'type', 'key' and
// 'relatedModel'. Options include 'includeInJSON', 'createModels' and 'reverseRelation'.
relations: [
{
type: Backbone.HasMany, // Use the type, or the string 'HasOne' or 'HasMany'.
key: 'occupants',
relatedModel: 'Person',
includeInJSON: Backbone.Model.prototype.idAttribute,
collectionType: 'PersonCollection',
reverseRelation: {
key: 'livesIn'
}
}
]
});
Person = Backbone.RelationalModel.extend({
relations: [
{ // Create a (recursive) one-to-one relationship
type: Backbone.HasOne,
key: 'user',
relatedModel: 'User',
reverseRelation: {
type: Backbone.HasOne,
key: 'person'
}
}
],
initialize: function() {
// do whatever you want :)
}
});
PersonCollection = Backbone.Collection.extend({
url: function( models ) {
// Logic to create a url for the whole collection, or a set of models.
// See the tests, or Backbone-tastypie, for an example.
return '/person/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
}
});
User = Backbone.RelationalModel.extend();
HasMany.onChangeto eliminate unnecessary events.
add,
mergeand
removeoptions on
Collection.addwhen working with RelationalModels. This also works when using
setto change the key on nested relations.
findOrCreateoption
updateis renamed to
merge, since it's behavior corresponds with
mergeon
Collection.add(and not with
updateon
Collection.reset).
update:<key>event has been removed, in favor of handling everything using "standard"
change:<key>events.
parseoption to relations.
autoFetchproperty for relations.
updateoption to
findOrCreate.
The original version of Backbone-relational! This already contained much of the basics: HasOne
and HasMany
relations (including reverseRelation
), Backbone.RelationalModel
and Backbone.Store
.
Each Backbone.RelationalModel registers itself with Backbone.Relational.Store upon
creation, and is removed from the store
when destroyed. When creating or updating an
attribute that is a key in a relation, removed related objects are notified of their
removal, and new related objects are looked up in the Store.
Backbone-relational only allows the existence of one model instance for each model type id
.
This check is there to enforce there will only be one version of a model with a certain id at any given time
(which is also the reason for the existence of Backbone.Relational.Store). This is necessary to enforce consistency
and integrity of relations.
If we were to allow multiple versions, inadvertently manipulating or performing a save, destroy or whatever on another version of that model (which is still around on the client, and can for example still be bound to one or more views in your application, either on purpose or inadvertently) would save it's state to the server, killing it's relations, and the server response would set the same (incorrect) data on the 'current' version of the model on the client. By then, you'd be in trouble.
Therefore, Backbone-relational simply does not allow this situation to occur. This is much safer than putting the burden on the
developer to always make sure every older version of a model is completely decoupled from every other part of your application.
It might be annoying to get an error every now and then, and sometimes inconvenient to have to use the factory method findOrCreate
,
but it's much better than subtle bugs that can lead to major data loss later on in the life cycle of your application.