MongoDB & Mongoose: Compatibility and Comparison
Rate this article
We’ll see how the MongoDB Schema Validation helps us enforce a database schema, while still allowing for great flexibility when needed. Finally, we’ll see if the additional features that Mongoose provides are worth the overhead of introducing a third-party library into our applications.
Mongoose is a Node.js-based Object Data Modeling (ODM) library for MongoDB. It is akin to an Object Relational Mapper (ORM) such as for traditional SQL databases. The problem that Mongoose aims to solve is allowing developers to enforce a specific schema at the application layer. In addition to enforcing a schema, Mongoose also offers a variety of hooks, model validation, and other features aimed at making it easier to work with MongoDB.
makes it possible to easily enforce a schema against your MongoDB database, while maintaining a high degree of flexibility, giving you the best of both worlds. In the past, the only way to enforce a schema against a MongoDB collection was to do it at the application level using an ODM like Mongoose, but that posed significant challenges for developers.
A huge benefit of using a NoSQL database like MongoDB is that you are not constrained to a rigid data model. You can add or remove fields, nest data multiple layers deep, and have a truly flexible data model that meets your needs today and can adapt to your ever-changing needs tomorrow. But being too flexible can also be a challenge. If there is no consensus on what the data model should look like, and every document in a collection contains vastly different fields, you're going to have a bad time.
On one end of the spectrum, we have ODM's like Mongoose, which from the get-go force us into a semi-rigid schema. With Mongoose, you would define a
Schemaobject in your application code that maps to a collection in your MongoDB database. The
Schemaobject defines the structure of the documents in your collection. Then, you need to create a
Modelobject out of the schema. The model is used to interact with the collection.
For example, let's say we're building a blog and want to represent a blog post. We would first define a schema and then create an accompanying Mongoose model:
Once we have a Mongoose model defined, we could run queries for fetching,updating, and deleting data against a MongoDB collection that alignswith the Mongoose model. With the above model, we could do things like:
The benefit of using Mongoose is that we have a schema to work against in our application code and an explicit relationship between our MongoDB documents and the Mongoose models within our application. The downside is that we can only create blog posts and they have to follow the above defined schema. If we change our Mongoose schema, we are changing the relationship completely, and if you're going through rapid development, this can greatly slow you down.
The other downside is that this relationship between the schema and model only exists within the confines of our Node.js application. Our MongoDB database is not aware of the relationship, it just inserts or retrieves data it is asked for without any sort of validation. In the event that we used a different programming language to interact with our database, all the constraints and models we defined in Mongoose would be worthless.
We simply write queries against the database and collection we wish to work with to accomplish the business goals. If we wanted to insert a new blog post in our collection, we could simply execute a command like so:
insertOne()operation would run just fine using the Node.js Driver. If we tried to save this data using our Mongoose
Blogmodel, it would fail, because we don't have an
authorproperty defined in our Blog Mongoose model.
We could then use this model in conjunction with our MongoDB Node.js driver, giving us both the flexibility of using the model, but not being constrained by it.
In this scenario, our MongoDB database is still blissfully unaware of our Blog model at the application level, but our developers can work with it, add specific methods and helpers to the model, and would know that this model is only meant to be used within the confines of our Node.js application. Next, let's explore schema validation.
We can choose between two different ways of adding schema validation to our MongoDB collections. The first is to use application-level validators, which are defined in the Mongoose schemas. The second is to use MongoDB schema validation, which is defined in the MongoDB collection itself. The huge difference is that native MongoDB schema validation is applied at the database level. Let's see why that matters by exploring both methods.
When it comes to schema validation, Mongoose enforces it at the application layer as we've seen in the previous section. It does this in two ways.
First, by defining our model, we are explicitly telling our Node.js application what fields and data types we'll allow to be inserted into a specific collection. For example, our Mongoose Blog schema defines a
titleproperty of type
String. If we were to try and insert a blog post with a
titleproperty that was an array, it would fail. Anything outside of the defined fields, will also not be inserted in the database.
Second, we further validate that the data in the defined fields matches our defined set of criteria. For example, we can expand on our Blog model by adding specific validators such as requiring certain fields, ensuring a minimum or maximum length for a specific field, or coming up with our custom logic even. Let's see how this looks with Mongoose. In our code we would simply expand on the property and add our validators:
Mongoose takes care of model definition and schema validation in one fell swoop. The downside though is still the same. These rules only apply at the application layer and MongoDB itself is none the wiser.
We can create a schema validation when creating our collection or after the fact on an existing collection. Since we've been working with this blog idea as our example, we'll add our schema validations to it. I will use Compass and . For a great resource on how to programmatically add schema validations, check out this .
Create a collection called
postsand let's insert our two documents that we've been working with. The documents are:
Now, within the Compass UI, I will navigate to the Validation tab. As expected, there are currently no validation rules in place, meaning our database will accept any document as long as it is valid BSON. Hit the Add a Rule button and you'll see a user interface for creating your own validation rules.
By default, there are no rules, so any document will be marked as passing. Let's add a rule to require the
authorproperty. It will look like this:
Now we'll see that our initial post, that does not have an
authorfield has failed validation, while the post that does have the
authorfield is good to go.
We can go further and add validations to individual fields as well. Say for SEO purposes we wanted all the titles of the blog posts to be a minimum of 20 characters and have a maximum length of 80 characters. We can represent that like this:
Now if we try to insert a document into our
postscollection either via the Node.js Driver or via Compass, we will get an error.
With Mongoose, our data model and schema are the basis for our interactions with MongoDB. MongoDB itself is not aware of any of these constraints, Mongoose takes the role of judge, jury, and executioner on what queries can be executed and what happens with them.
But with MongoDB native schema validation, we have additional flexibility. When we implement a schema, validation on existing documents does not happen automatically. Validation is only done on updates and inserts. If we wanted to leave existing documents alone though, we could change the
validationLevelto only validate new documents inserted in the database.
Additionally, with schema validations done at the MongoDB database level, we can choose to still insert documents that fail validation. The
validationActionoption allows us to determine what happens if a query fails validation. By default, it is set to
error, but we can change it to
warnif we want the insert to still occur. Now instead of an insert or update erroring out, it would simply warn the user that the operation failed validation.
And finally, if we needed to, we can bypass document validation altogether by passing the
bypassDocumentValidationoption with our query. To show you how this works, let's say we wanted to insert just a
postscollection and we didn't want any other data. If we tried to just do this...
... we would get an error saying that document validation failed. But if we wanted to skip document validation for this insert, we would simply do this:
This would not be possible with Mongoose. MongoDB schema validation is more in line with the entire philosophy of MongoDB where the focus is on a flexible design schema that is quickly and easily adaptable to your use cases.
The final area where I would like to compare Mongoose and the Node.js MongoDB driver is its support for pseudo-joins. Both Mongoose and the native Node.js driver support the ability to combine documents from multiple collections in the same database, similar to a join in traditional relational databases.
The Mongoose approach is called Populate. It allows developers to create data models that can reference each other and then, with a simple API, request data from multiple collections. For our example, let's expand on the blog post and add a new collection for users.
What we did above was we created a new model and schema to represent users leaving comments on blog posts. When a user leaves a comment, instead of storing information on them, we would just store that user’s
_id. So, an update operation to add a new comment to our post may look something like this:
This is assuming that we have a user in our
Usercollection with the
12345. Now, if we wanted to populate our
userproperty when we do a query—and instead of just returning the
_idreturn the entire document—we could do:
Populate coupled with Mongoose data modeling can be very powerful, especially if you're coming from a relational database background. The drawback though is the amount of magic going on under the hood to make this happen. Mongoose would make two separate queries to accomplish this task and if you're joining multiple collections, operations can quickly slow down.
The other issue is that the populate concept only exists at the application layer. So while this does work, relying on it for your database management can come back to bite you in the future.
MongoDB as of version 3.2 introduced a new operation called
$lookupthat allows to developers to essentially do a left outer join on collections within a single MongoDB database. If we wanted to populate the user information using the Node.js driver, we could create an aggregation pipeline to do it. Our starting point using the
$lookupoperator could look like this:
Both Mongoose and the MongoDB Node.js driver support similar functionality. While Mongoose does make MongoDB development familiar to someone who may be completely new, it does perform a lot of magic under the hood that could have unintended consequences in the future.
I personally believe that you don't need an ODM to be successful with MongoDB. I am also not a huge fan of ORMs in the relational database world. While they make initial dive into a technology feel familiar, they abstract away a lot of the power of a database.
Developers have a lot of choices to make when it comes to building applications. In this article, we looked at the differences between using an ODM versus the native driver and showed that the difference between the two is not that big. Using an ODM like Mongoose can make development feel familiar but forces you into a rigid design, which is an anti-pattern when considering building with MongoDB.
The MongoDB Node.js driver works natively with your MongoDB database to give you the best and most flexible development experience. It allows the database to do what it's best at while allowing your application to focus on what it's best at, and that's probably not managing data models.