grapher/docs/linking_collections.md

468 lines
11 KiB
Markdown
Raw Normal View History

2017-11-30 22:11:43 +02:00
# Linking Collections
2017-12-01 12:30:09 +02:00
Let's learn what type of links we can define between collections, and what is the best way to do them.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
First, we begin with an illustration of the power of Grapher.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
Let's assume our `posts` collection, contains a field, called `authorId` which represents an actual `_id` from `Meteor.users`,
if you wanted to get the post and the author's name, you had to first fetch the post, and then get the author's name based on his `_id`.
2017-11-30 22:11:43 +02:00
```js
Meteor.methods({
getPost({postId}) {
let post = Posts.findOne(postId, {
fields: {
title: 1,
createdAt: 1,
authorId: 1,
}
});
if (!post) { throw new Meteor.Error('not-found') }
const author = Meteor.users.findOne(post.authorId, {
fields: {
firstName: 1,
lastName: 1
}
});
Object.assign(post, {author});
return post;
}
})
```
With Grapher, your code above is transformed to:
```js
Meteor.methods({
getPost({postId}) {
let post = Posts.createQuery({
$filters: {_id: postId},
title: 1,
createdAt: 1,
author: {
firstName: 1,
lastName: 1
}
});
return post.fetchOne();
}
})
```
This is just a simple illustration, imagine the scenario, in which you had comments,
and the comments had authors, and you needed their avatar. Your code can easily
2017-12-01 12:30:09 +02:00
grow very large, and it's going to be hard to make it performant.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
## A basic link
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
To define the link illustrated in the example above, we create a separate `links.js` file that is
imported separately outside the collection module. We need to define the links in their own module,
2017-12-01 13:11:06 +02:00
after all collections have been defined, because there will be situations where, when you define the links
where you define the collection, 2 collections import each other, leading to some strange behaviors.
2017-11-30 22:11:43 +02:00
```js
// file: /imports/db/posts/links.js
import Posts from '...';
Posts.addLinks({
'author': {
type: 'one',
collection: Meteor.users,
field: 'authorId',
}
})
```
2017-12-01 12:30:09 +02:00
You created the link, and now you can use the query illustrated above.
We decided to choose `author` as a name for our link and `authorId` the field to store it in, but it's up to you to decide this.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
## Inversed links
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
Because we linked `Posts` with `Meteor.users` it means that we can also get all `posts` of an user.
Because in a way `Meteor.users` is also linked with `Posts` but an `inversed` way. We refer to it as an `Inversed Link`.
2017-11-30 22:11:43 +02:00
```js
// file: /imports/db/users/links.js
import Posts from '...';
Meteor.users.addLinks({
'posts': {
collection: Posts,
inversedBy: 'author'
}
})
```
2017-12-01 12:30:09 +02:00
`author` represents the link name that was defined inside Posts. Defining inversed links allows us to do:
2017-11-30 22:11:43 +02:00
```js
Meteor.users.createQuery({
posts: {
title: 1
}
})
```
2017-12-01 12:30:09 +02:00
## One and Many
2017-11-30 22:11:43 +02:00
Above you've noticed a `type: 'one'` in the link definition, but let's say we have a `Post` that belongs to many `Categories`,
which have their own collection into the database. This means that we need to relate with more than a single element.
```js
// file: /imports/db/posts/links.js
import Posts from '...';
import Categories from '...';
Posts.addLinks({
'categories': {
type: 'many',
collection: Categories,
field: 'categoryIds',
}
})
```
2017-12-01 12:30:09 +02:00
In this case, `categoryIds` is an array of strings (`[categoryId1, categoryId2, ...]`), each string, representing `_id` from `Categories` collection.
Let's also create an inversed link from `Categories`, so you can use it inside the `query`
2017-11-30 22:11:43 +02:00
```js
// file: /imports/db/posts/links.js
import Categories from '...';
import Posts from '...';
Categories.addLinks({
'posts': {
collection: Posts,
inversedBy: 'categories'
}
})
```
2017-12-01 12:30:09 +02:00
By defining this, I can query for a category, and get all the posts it has.
2017-11-30 22:11:43 +02:00
## Meta Links
We use a `meta` link when we want to add additional data about the relationship. For example,
2017-12-01 12:30:09 +02:00
a user can belong in a single `Group`, but we need to also store when he joined that group and what roles he has in it.
2017-11-30 22:11:43 +02:00
```js
// file: /imports/db/users/links.js
import Groups from '...'
Meteor.users.addLinks({
group: {
type: 'one',
collection: Groups,
field: 'groupLink',
metadata: true,
}
})
```
Notice the new option `metadata: true` this means that `groupLink` is no longer a `String`, but an `Object` that looks like this:
```
// inside a Meteor.users document
{
...
groupLink: {
2017-12-01 12:30:09 +02:00
_id: 'XXX', // This is the _id of the group
2017-11-30 22:11:43 +02:00
roles: 'ADMIN',
createdAt: Date,
}
}
```
Let's see how this works out in our query:
```js
const user = Meteor.users.createQuery({
$filters: {_id: userId},
group: {
name: 1,
}
}).fetchOne()
```
`user` will look like this:
```
{
_id: userId,
group: {
$metadata: {
roles: 'ADMIN',
createdAt: Date
},
name: 'My Funky Group'
}
}
```
2017-12-01 12:30:09 +02:00
We store the metadata of the link inside a special `$metadata` field. And it works from inversed side as well:
2017-11-30 22:11:43 +02:00
```js
Groups.addLinks({
users: {
collection: Meteor.users,
inversedBy: 'group'
}
});
2017-12-01 12:30:09 +02:00
```
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
```js
2017-11-30 22:11:43 +02:00
const group = Groups.createQuery({
$filters: {_id: groupId},
name: 1,
users: {
firstName: 1,
}
}).fetchOne()
```
`group` will look like:
```
{
_id: groupId,
name: 'My Funky Group',
users: [
{
$metadata: {
roles: 'ADMIN',
createdAt: Date
},
_id: userId,
firstName: 'My Funky FirstName',
2017-12-01 12:30:09 +02:00
},
...
2017-11-30 22:11:43 +02:00
]
}
```
The same principles apply to `meta` links that are `type: 'many'`, if we change that in the example above.
The storage field will look like:
```
{
groupLinks: [
{_id: 'groupId', roles: 'ADMIN', createdAt: Date},
...
]
}
```
2017-12-01 12:30:09 +02:00
The same principles above apply, we still store `$metadata` field.
2017-11-30 22:11:43 +02:00
I know what question comes to your mind right now, what if I want to put a field inside the metadata,
2017-12-01 12:30:09 +02:00
which represents an id from other collection, like I want to store who added that user (`addedBy`) to the group.
No-one stops you from storing it as a string, but if this is one of Grapher's current limitations, it can't fetch
links that are inside the metadata.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
But Grapher makes you think relational again, and you can abstract it to another collection:
2017-11-30 22:11:43 +02:00
```js
// file: /imports/db/groupUserLinks/links.js
import Groups from '...';
import GroupUserLinks from '...';
2017-12-01 12:30:09 +02:00
// file: /imports/db/users/links.js
Meteor.users.addLinks({
groupLink: {
collection: GroupUserLinks,
type: 'one',
field: 'groupLinkId',
}
})
2017-11-30 22:11:43 +02:00
GroupUserLinks.addLinks({
user: {
collection: Meteor.users,
2017-12-01 12:30:09 +02:00
inversedBy: 'groupLink',
2017-11-30 22:11:43 +02:00
},
adder: {
type: 'one',
collection: Meteor.users,
field: 'addedBy'
},
group: {
type: 'one',
collection: Meteor.users,
field: 'groupId'
}
})
```
And the query will look like this:
```js
Meteor.users.createQuery({
groupLink: {
group: {
name: 1,
},
adder: {
firstName: 1
},
roles: 1,
createdAt: 1,
}
})
```
## Link Loopback
No one stops you from linking a collection to itself, say you have a list of friends which are also users:
2017-12-01 12:30:09 +02:00
2017-11-30 22:11:43 +02:00
```js
Meteor.users.addLinks({
friends: {
collection: Meteor.users,
type: 'many',
field: 'friendIds',
}
});
```
Say you want to get your friends, and friends of friends, and friends of friends of friends!
```js
Meteor.users.createQuery({
$filters: {_id: userId},
friends: {
nickname: 1,
friends: {
nickname: 1,
friends: {
nickname: 1,
}
}
}
});
```
## Uniqueness
2017-12-01 12:30:09 +02:00
The `type: 'one'` doesn't necessarily guarantee uniqueness from the inversed side.
For example, we have `Comments` and `Posts` linked, by defining a `one` link from Comments to Posts,
and an inversed link from Posts to Comments.
When you fetch comments, from posts, the inversed side, they will return an array.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
But if you want to have a `OneToOne` relationship, and you want Grapher to give you a single object in return,
you can do:
2017-11-30 22:11:43 +02:00
```js
Meteor.users.addLinks({
paymentProfile: {
collection: PaymentProfiles,
inversedBy: 'user'
}
});
PaymentProfiles.addLinks({
user: {
field: 'userId',
collection: Meteor.users,
type: 'one',
unique: true
}
})
```
Now fetching:
```js
Meteor.users.createQuery({
paymentProfile: {
type: 1,
last4digits: 1,
}
});
```
`paymentProfile` inside `user` will be an object because it knows it should be unique.
## Data Consistency
2017-12-01 12:30:09 +02:00
We clean out leftover links from deleted collection items.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
Let's say I have a `Threads` collection with some `memberIds` linked to `Meteor.users`.
If a `user` is deleted from the database, we don't want to keep unexisting references.
2017-11-30 22:11:43 +02:00
So after we delete a user, all threads containing that users should be cleaned.
This is done automatically by Grapher so you don't have to deal with it.
2017-12-01 12:30:09 +02:00
The only rule is that `Meteor.users` collection needs to have an inversed link to `Threads`.
2017-11-30 22:11:43 +02:00
In conclusion, if you want to benefit of this, you have to define inversed links for every direct links.
## Autoremoval
```js
Meteor.users.addLinks({
'posts': {
collection: Posts,
inversedBy: 'author',
autoremove: true
}
});
```
2017-12-01 12:30:09 +02:00
After you delete a user, all the links that have `autoremove: true` will be deleted.
This works from the `direct` side as well, not only from `inversed` side.
2017-11-30 22:11:43 +02:00
2017-12-01 12:30:09 +02:00
Please use with caution, sometimes it's better to explicitly delete it, but there will be situations,
where you don't care and this makes your code cleaner.
2017-11-30 22:11:43 +02:00
## Indexing
As a rule of thumb, you must index all of your links. Because that's how you achieve absolute performance.
This is not done by default, to allow the developer flexibility, but you can do it simply enough from the direct side definition of the link:
```js
PaymentProfiles.addLinks({
user: {
field: 'userId',
collection: Meteor.users,
type: 'one',
unique: true,
index: true,
}
})
```
2017-12-01 12:30:09 +02:00
The index is applied only on the `_id`, meaning that if you have `meta` links, other fields present in that object will not be indexed,
but you can run a `Collection._ensureIndex` separately.
2017-11-30 22:11:43 +02:00
If you have `unique: true` set, the index will also apply a unique constraint to it.
## Top Level Fields
2017-12-01 12:30:09 +02:00
Grapher currently supports only top level fields for storing linking data.
One of the reasons it doesn't allow nested fields is to enforce the developer to think relational and
eliminate large and complex documents by abstracting them into collections.
2017-11-30 22:11:43 +02:00
In the future, this limitation may change, but for now you can work around this and keep your code elegant.
2017-12-01 12:58:36 +02:00
## Conclusion
2017-11-30 22:11:43 +02:00
Using these simple techniques, you can create a beautiful database schemas inside MongoDB that are relational and very simple to fetch,
you will eliminate almost all your boilerplate code around this and allows you to focus on more important things.
2017-12-01 12:58:36 +02:00
## [Continue Reading](linker_engine.md) or [Back to Table of Contents](index.md)
2017-11-30 22:11:43 +02:00