7. Relations
Describing how your Resources are related
This tutorial needs review
Getting started
This page assumes you've read part 6 of the tutorial, Schemas & Validation.
Relations describe the associations between your different Resources. For example, many Comments could be associated with a single blog Post. Many blog Posts could be associated with a single User. A single User could be associated with many Comments. A single User could be associated with many blog Posts. The type of relationship depends on the direction from which you describe it.
You do not have to define relations in order to use JSData, but doing so allows you to use some convenience features of JSData.
Tip
There are essentially two types of relationships: one-to-one and one-to-many.
many-to-many relationships are generally achieved through some combination of one-to-many and one-to-one relationships.
Defining relations
Relations are defined as part of a Mapper's configuration. When defining a Mapper, add a relations
property to the options
object:
import { Container } from 'js-data';
const store = new Container();
store.defineMapper('user', {
relations: {
hasMany: {
post: {
// In a hasMany relationship configured with
// a foreignKey, the foreignKey specifies the
// property of the child record that points
// to the parent record,
// i.e. console.log(post.user_id); // 12345
foreignKey: 'user_id',
// In memory, a user's posts will be attached to
// user objects via the user's "posts" property,
// i.e. console.log(user.posts); // [{...}, {...}, ...]
// and console.log(user.posts[0].user_id); // 12345
localField: 'posts'
}
}
}
});
store.defineMapper('post', {
relations: {
belongsTo: {
user: {
// In a belongsTo relationship, the foreignKey
// specifies the property of the child record
// that points to the parent record,
// i.e. console.log(post.user_id); // 12345
foreignKey: 'user_id',
// In memory, a post's user will be attached to
// post objects via the post's "user" property,
// i.e. console.log(post.user); // {...}
// and console.log(post.user[post.user_id]); // 12345
localField: 'user'
}
}
}
});
// some data representations
const user = {
id: 1,
posts: [{ id: 34, user_id: 1 }, { id: 35, user_id: 1 }]
};
const post = {
id: 34,
user_id: 1,
user: {
id: 1
}
};
Tip
It's important to define both sides of a relationship. Notice that in the sample above there are two relation definitions: user hasMany post and post belongsTo user. By defining both sides of the relationship, JSData can load a user's posts or load a post's user, as well link them together in memory for convenient relationship traversal.
Canonical relationship examples
These three examples capture the majority of possible relation configurations:
User hasMany (foreignKey) Postjs store.defineMapper('user', { relations: { hasMany: { post: { foreignKey: 'user_id', localField: 'posts' } } } }); const user = { id: 1212 }; Example user record with its posts embedded: js const user = { id: 1212, posts: [ { id: 3343, user_id: 1212 }, { id: 3344, user_id: 1212 } ] }; | Post belongsTo (foreignKey) Userjs store.defineMapper('post', { relations: { belongsTo: { user: { foreignKey: 'user_id', localField: 'user' } } } }); const posts = [ { id: 3343, user_id: 1212 }, { id: 3344, user_id: 1212 } ]; Example post record with its user embedded: js const post = { id: 3343, user_id: 1212, user: { id: 1212 } }; |
User hasMany (localKeys) Postjs store.defineMapper('user', { relations: { hasMany: { post: { localKeys: 'post_ids', localField: 'posts' } } } }); const user = { id: 1212, post_ids: [3343, 3344] }; Example user record with its posts embedded: js const user = { id: 1212, post_ids: [3343, 3344], posts: [ { id: 3343 }, { id: 3344 } ] }; | Post hasMany (foreignKeys) Userjs store.defineMapper('post', { relations: { belongsTo: { user: { foreignKeys: 'post_ids', localField: 'users' } } } }); const post = [ { id: 3343 }, { id: 3344 } ]; Example post record with its user embedded: js const post = { id: 3343, user: { id: 1212, post_ids: [3343, 3344] } }; |
User hasOne (foreignKey) Profilejs store.defineMapper('user', { relations: { hasOne: { profile: { foreignKey: 'user_id', localField: 'profile' } } } }); const user = { id: 1212 }; Example user record with its profile embedded: js const user = { id: 1212, profile: { id: 5545, user_id: 1212 } }; | Profile belongsTo (foreignKey) Userjs store.defineMapper('profile', { relations: { belongsTo: { user: { foreignKey: 'user_id', localField: 'user' } } } }); const profile = { id: 5545, user_id: 1212 }; Example profile record with its user embedded: js const profile = { id: 5545, user_id: 1212, user: { id: 1212 } }; |
belongsTo relationship
The belongsTo relationship defines the association between a child record and its parent record. The relationship information is typically stored in a "foreign key" on the child record, meaning, the child record has a field whose value is the primary key of the parent record. A belongsTo relationship can be paired with a hasMany relationship that uses the foreignKey
option or a hasOne relationship.
Example
store.defineMapper('post', {
relations: {
belongsTo: {
user: {
foreignKey: 'user_id',
localField: 'user'
}
}
}
});
store.defineMapper('user', {
relations: {
hasMany: {
post: {
foreignKey: 'user_id',
localField: 'posts'
}
}
}
});
store.defineMapper('profile', {
relations: {
// Profile belongsTo User
belongsTo: {
user: {
foreignKey: 'user_id',
localField: 'user'
}
}
}
});
store.defineMapper('user', {
relations: {
// User hasOne Profile
hasOne: {
post: {
foreignKey: 'user_id',
localField: 'profile'
}
}
}
});
hasMany relationship
The hasMany relationship defines a parent -> child relationship, where the parent zero or more children. hasMany relationships can be defined 3 ways: foreignKey
, localKeys
, and foreignKeys
.
foreignKey
Defining a hasMany relationship using the foreignKey
option means that the child records each have a field whose value is the primary key of the parent. A hasMany relationship that uses the foreignKey
option is usually paired with a belongsTo relationship.
Example
store.defineMapper('user', {
relations: {
// User hasMany Post
hasMany: {
post: {
foreignKey: 'user_id',
localField: 'posts'
}
}
}
});
store.defineMapper('post', {
relations: {
// Post belongsTo User
belongsTo: {
user: {
foreignKey: 'user_id',
localField: 'user'
}
}
}
});
localKeys
Defining a hasMany relationship using the localKeys
option means that the parent record has a field whose value is an array of the primary keys of the parent's child records. A hasMany relationship that uses the localKeys
option is usually paired with another hasMany relationship that uses the foreignKeys
option.
Example
store.defineMapper('user', {
relations: {
// User hasMany Post
hasMany: {
post: {
localKeys: 'post_ids',
localField: 'posts'
}
}
}
});
store.defineMapper('post', {
relations: {
// Post hasMany User
hasMany: {
user: {
foreignKeys: 'post_ids',
localField: 'users'
}
}
}
});
foreignKeys
Defining a hasMany relationship using the foreignKeys
option means that the child records do not have references to their parent records, because the parent records have a field whose value is an array for the primary keys of its child records. hasMany relationships that use the foreignKeys
option are usually paired with a hasMany relationship that uses the localKeys
option.
Example
store.defineMapper('post', {
relations: {
// Post hasMany User
hasMany: {
user: {
foreignKeys: 'post_ids',
localField: 'users'
}
}
}
});
store.defineMapper('user', {
relations: {
// User hasMany Post
hasMany: {
post: {
localKeys: 'post_ids',
localField: 'posts'
}
}
}
});
hasOne relationship
The hasOne relationship defines a parent -> child relationship, where the parent has a single child. A hasOne relationship is usually paired with a belongsTo relationship.
Example
store.defineMapper('user', {
relations: {
// User hasOne Profile
hasOne: {
post: {
foreignKey: 'user_id',
localField: 'profile'
}
}
}
});
store.defineMapper('profile', {
relations: {
// Profile belongsTo User
belongsTo: {
user: {
foreignKey: 'user_id',
localField: 'user'
}
}
}
});
Eagerly loading relations
Eager-loading relations is done via a with
option passed to find
or findAll
. The with
option should be an array of strings, which are either the name
of a related Mapper, or the localField
where a relation is defined. To load deeper relations, use a period, e.g. posts.comments
. In order for loading deep relations to work, you must also include the intermediate relations in the array of strings, e.g. ['posts', 'posts.comments']
.
Examples
store.find('user', 123, { with: ['post', 'profile'] })
.then((user) => {
console.log(user); // { id: 123, ... }
console.log(user.profile); // { id: 345, user_id: 123, ... }
console.log(user.posts); // [{ id: 5765, user_id: 123, ... }, ...]
});
store.findAll('user', { status: 'active' }, { with: ['posts', 'post.comments', 'profile'] })
.then((users) => {
console.log(users[0]); // { id: 123, ... }
console.log(users[0].profile); // { id: 345, user_id: 123, ... }
console.log(users[0].posts); // [{ id: 5765, user_id: 123, ... }, ...]
console.log(users[0].posts[0].comments); // [{ id: 74756, post_id: 5765, ... }, ...]
});
store.find('post', 5765, { with: ['user', 'user.profile', 'comments'] })
.then((post) => {
console.log(post); // { id: 5765, user_id: 123, ... }
console.log(post.user); // { id: 123, ... }
console.log(post.user.profile); // { id: 345, user_id: 123, ... }
console.log(post.comments); // [{ id: 74564, post_id: 5765, ... }, ...]
});
Lazily loading relations
Loading relations in a lazy fashion can easily be done by making your own find
and findAll
calls. That's essentially what JSData does with the loadRelations
convenience method. Record#loadRelations
is a convenience method that lazily loads a record's relations.
Examples
store.find('user', 123)
.then((user) => {
return store.find('post', { user_id: user.id });
})
.then((posts) => {
return Promise.all(posts.map((post) => store.findAll('comment', { post_id: post.id })));
});
console.log(user); // { id: 123, ... }
console.log(user.profile); // undefined
console.log(user.posts); // undefined
user.loadRelations(['posts', 'posts.comments', 'profile']).then((user) => {
console.log(user); // { id: 123, ... }
console.log(user.profile); // { id: 345, user_id: 123, ... }
console.log(user.posts); // [{ id: 5765, user_id: 123, ... }, ...]
console.log(user.posts[0].comments); // [{ id: 74564, post_id: 5765, ... }, ...]
});
Deep-creating relations
Sometimes you need to create multiple related records all at once. Here's an example of doing it manually:
const postProps = {
title: 'Relations'
};
const commentProps = [
{
content: 'Awesome!'
},
{
content: 'So cool!'
}
];
store.create('post', postProps)
.then((post) => {
commentProps.forEach((comment) => {
comment.post_id = post.id;
});
return store.createMany('comment', commentProps);
})
.then((comments) => {
// done
});
Mapper#create and Mapper#createMany support shorthands for this:
const postProps = {
title: 'Relations',
comments: [
{
content: 'Awesome!'
},
{
content: 'So cool!'
}
]
};
store.create('post', postProps, { with: ['comments'] })
.then((post) => {
// done
post.id === post.comments[0].post_id; // true!
});
Using the with
option in the context of create
or createMany
results in a cascading create if the props
contain embedded relations. A cascading create means JSData will be making multiple create
/createMany
calls for you.
Caution
Cascading creates are NOT performed in a transaction. You'll have to do that yourself.
In the case of the HTTP adapter, you might want to send the embedded relations to your server instead of JSData making multiple create
/createMany
calls for you. In this case use the pass
option:
const postProps = {
title: 'Relations',
comments: [
{
content: 'Awesome!'
},
{
content: 'So cool!'
}
]
};
store.create('post', postProps, { pass: ['comments'] })
.then((post) => {
// done
post.id === post.comments[0].post_id; // true!
});
And then on the server you can sort out the embedded relations and do the cascading create calls yourself.
Next steps: Working with the DataStore
Move on to part 8 of the tutorial: Working with the DataStore
See an issue with this tutorial?
You can open an issue or better yet, suggest edits right on this page.
Need support?
Have a technical question? Post on the js-data Stack Overflow channel or the Mailing list.
Want to chat with the community? Hop onto the js-data Slack channel.
Updated over 7 years ago
Continue with part 8 of the Tutorial or explore other documentation.