A crash course guide on working with MongoDB using the Mongoose ORM
Contents
- Introduction to MongoDB
- Getting Started
- MongoDB Compass
- MongoDB Atlas
- Using Homebrew (Optional)
- Using Mongoose to Connect to MongoDB Database
- Schemas
- Models
- Saving a Document
- CRUD and Querying
- Validation
- Modeling Relationships
- Referencing a Document
- Embedding a Document
- Transactions
- Learning Resources
Introduction to MongoDB
MongoDB is one of the most popular NoSQL databases and it stores data in flexible, JSON-like documents.
In relational databases we have tables and rows, in MongoDB we have collections and documents. A document can contain sub-documents, but there is no real relationship between documents.
Getting Started
To get started, you can go to MongoDB's website where you can create an account, create a free cluster for your database and choose where to view your cluster. Typically, I go with mongoDB Compass as the viewport since it has a very simple GUI to use.
MongoDB Compass
MongoDB Compass is a powerful GUI for querying, aggregating, and analyzing your MongoDB data in a visual environment.
Install MongoDB Compass: MongoDB Compass | MongoDB
MongoDB Atlas
MongoDB Atlas is a multi-cloud database service by the same people that build MongoDB. Atlas simplifies deploying and managing your databases while offering the versatility you need to build resilient and performant global applications on the cloud providers of your choice.
Install MongoDB Using Homebrew (Optional)
An alternative way to use mongo is by downloading it through your terminal. Make sure to have homebrew installed to do this.
Install Homebrew: The Missing Package Manager for macOS (or Linux) — Homebrew
homebrew install mongodb
Using Mongoose to Connect to MongoDB
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and is used to translate between objects in code and the representation of those objects in MongoDB.
Install mongoose from the command line:
npm install mongoose
Mongoose has a built in connect() function that helps connect to your database.
In your index.js or backend file, your code should look something like this:
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/db')
//Replace db with the name of your database
.then(() => console.log('Connected...'))
.catch(err => console.error('Connection failed...'));
Schemas
To store objects in MongoDB, we need to define a Mongoose schema first. The schema defines the shape of documents in MongoDB.
Use new mongoose.Schema() to define the object details:
const courseSchema = new mongoose.Schema({
name: String,
price: Number
});
We can use a SchemaType object to provide additional details.
Supported types are:
String, Number, Date, Buffer (for storing binary data), Boolean and ObjectID.
const courseSchema = new mongoose.Schema({
isPublished: {
type: Boolean,
default: false
}
});
Once we have a schema, we need to compile it into a model.
Models
A model is like a class. It’s a blueprint for creating objects:
const Course = mongoose.model(‘Course’, courseSchema);
Saving a Document
In order to publish changes, we need to use the save() method:
let course = new Course({ name: 'Coding', price: 15 });
course = await course.save(); // This needs to be place in an async function
CRUD and Querying
CRUD stands for Create, Read, Update and Delete. This is how we make changes to data within a database.
A query can either be a request for data results from your database or for action on the data, or for both.
Here are some ways we can use queries and CRUD operations to change data:
// Querying documents
const courses = await Course
.find({ author: 'John', isPublished: true })
.skip(10)
.limit(10)
.sort({ name: 1, price: -1 })
.select({ name: 1, price: 1 });
//Updating a document (query first)
const course = await Course.findById(id);
if (!course) return;
course.set({ name: '...' })
course.save();
// Updating a document (update first)
const result = await Course.update({ _id: id }, {
$set: { name: '...' }
})
// Updating a document (update first) and return it
const result = await Course.findByIdAndUpdate({ _id: id }, {
$set: { name: '...' }
}, { new: true });
// Removing a document
const result = await Course.deleteOne({ _id: id });
const result = await Course.deleteMany({ _id: id });
const course = await Course.findByIdAndRemove(id);
Validation
When defining a schema, you can set the type of a property to a SchemaType object. You use this object to define the validation requirements for the given property.
// Adding validation
new mongoose.Schema({
name: { type: String, required: true }
})
Validation logic is executed by Mongoose prior to saving a document to the database. You can also trigger it manually by calling the validate() method.
Built-in validators:
Strings: minlength, maxlength, match, enum
Numbers: min, max
Dates: min, max
All types: required
You can also create your own validations:
// Custom validation
tags: [
type: Array,
validate: {
validator: function(v) { return v && v.length > 0; },
message: 'A course should have at least 1 tag.'
}]
If you need to talk to a database or a remote service to perform the validation, you need to create an async validator:
validate: {
isAsync: true
validator: function(v, callback) {
// Do the validation, when the result is ready, call the callback
callback(isValid);
}
}
Other useful SchemaType properties:
Strings: lowercase, uppercase, trim
All types: get, set (to define a custom getter/setter)
price: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v)
}
Modeling Relationships Between Data
To model relationships between connected data, we can either reference a document or embed it in another document.
When referencing a document, there is really no relationship between these two documents. So, it is possible to reference a non-existing document.
Referencing documents is called normalization and this is a good approach when you want to enforce data consistency. Because there will be a single instance of an object in the database. But this approach has a negative impact on the performance of your queries because in MongoDB we cannot JOIN documents as we do in relational databases. So, to get a complete representation of a document with its related documents, we need to send multiple queries to the database.
Denormalization is the process of embedding documents, which solves the above issue. We can read a complete representation of a document with a single query. All the necessary data is embedded in one document and its children. But this also means we’ll have multiple copies of data in different places. While storage is not an issue these days, having multiple copies means changes made to the original document may not propagate to all copies. If the database server dies during an update, some documents will be inconsistent.
For every business, for every problem, you need to ask this question: “can we tolerate data being inconsistent for a short period of time?” If not, you’ll have to use references. But again, this means that your queries will be slower.
Referencing a document
// Referencing a document
const courseSchema = new mongoose.Schema({
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Author'
}
})
// Referencing a document
const courseSchema = new mongoose.Schema({
author: {
type: new mongoose.Schema({
name: String,
bio: String
})
}
})
Embedding a document
Embedded documents don’t have a save() method. They can only be saved in the context of their parent.
// Updating an embedded document
const course = await Course.findById(courseId);
course.author.name = 'New Name';
course.save(); //can't do course.author.save
Transactions
We don’t have transactions in MongoDB. To implement transactions, we use a pattern called “Two Phase Commit”. If you don’t want to manually implement this pattern, use the Fawn NPM package:
try {
await new Fawn.Task()
.save('rentals', newRental)
.update('movies', { _id: movie._id }, { $inc: numberInStock: -1 })
.run()
}
catch (ex) {
// At this point, all operations are automatically rolled back
}
ObjectIDs are generated by MongoDB driver and are used to uniquely identify a document. They consist of 12 bytes:
4 bytes: timestamp
3 bytes: machine identifier
2 bytes: process identifier
3 bytes: counter
ObjectIDs are almost unique. In theory, there is a chance for two ObjectIDs to be equal but the odds are very low (1/16,000,000) for most real-world applications.
// Validating ObjectIDs
mongoose.Types.ObjectID.isValid(id);
ObjectIDs are almost unique. In theory, there is a chance for two ObjectIDs to be equal but the odds are very low (1/16,000,000) for most real-world applications.
Comentarios