An essential thing about MongoDB is that it is non-relational. Therefore, it might not be the best fit if relationships are a big part of our database design. That being said, we definitely can mimic SQL-style relations by using references of embedding documents directly.
You can get all of the code from this article in this repository.
In this article, we base the code on many of the functionalities we’ve implemented in the previous parts of this series. If you want to know how we register and authenticate users, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies.
A few significant things are happening above. We use unique: true above to make sure that all users have unique emails. It sets up unique indexes under the hood and deserves a separate article.
The @Exclude and @Transform decorators come from the class-transformer library. We cover serialization in more detail in API with NestJS #5. Serializing the response with interceptors. There is a significant catch here with MongoDB and Mongoose, though.
The Mongoose library that we use for connecting to MongoDB and fetching entities does not return instances of our User class. Therefore, the ClassSerializerInterceptor won’t work out of the box. Let’s change it a bit using the mixin pattern.
With the one-to-one relationship, the document in the first collection has just one matching document in the second collection and vice versa. Let’s create a schema for the address:
There is a big chance that just one user is assigned to a particular address in our application. Therefore, it is a good example of a one-to-one relationship. Because of that, we can take advantage of embedding documents, which is an approach very good performance-wise.
For it to work properly, we need to explicitly pass AddressSchema to the @Prop decorator:
We use @Type(() => Address) above to make sure that the class-transformer transforms the Address object too.
When we create the document for the user, MongoDB also creates the document for the address. It also gives it a distinct id.
In our one-to-one relationship example, the user has just one address. Also, one address belongs to only one user. Since that’s the case, it makes sense to embed the user straight into the user’s document. This way, MongoDB can return it fast. Let’s use MongoDB Compass to make sure that this is the case here.
We implement the one-to-many and many-to-one relationships when a document from the first collection can be linked to multiple documents from the second collection. Documents from the second collection can be linked to just one document from the first collection.
Great examples are posts and authors where the user can be an author of multiple posts. In our implementation, the post can only have one author, though.
In the code above, we store the id of the author in the document of the post.
We could do that the other way around and store the posts’ id in the author’s document.
When deciding that, we need to take a few factors into account.
First, we need to think of how many references we want to store.
Imagine a situation where we want to store logs for different machines in our server room. We need to remember that the maximum size of a MongoDB document is 16MB.
If we store an array of the ids of the Log document in the Machine document, in theory, we could run out of space at some point. We can store a single id of the machine in the Log document instead.
The other thing to think through is what queries we will run most often.
For example, in our implementation of posts and authors, it is effortless to retrieve the author’s data if we have the post.
This is thanks to the fact that we store the author’s id in the document of the post. On the other hand, it would be more time-consuming to retrieve a list of posts by a single user.
To do that, we would need to query all of the posts and check the author’s id.
We could implement two-way referencing and store the reference on both sides to deal with the above issue.
The above would speed up some of the queries but require us to put more effort into keeping our data consistent.
We could also embed the document of the posts into the document of the user. The advantage of doing that would be not performing additional queries to the database to get the missing information. But, unfortunately, this would make getting a particular post more difficult.
Another important relationship to consider is many-to-many. A document from the first collection can refer to multiple documents from the second collection and the other way around.
A good example would be posts that can belong to multiple categories. Also, a single category can belong to multiple posts. First, let’s define the schema of our category.