Database Operations
We've connected to the database, created a schema, and model. Now, we can perform CRUD operations on the database using the model.
Database Operations
First of all, we need to import the Model where we need to perform database operations if it's not in the same file. In this doc, I will be doing these operations:
Document and Record are the same thing. I will be using these two words interchangeably. In MongoDB, we call it Document, and in SQL, we call it Record.
Create Documents
Creating a document means inserting a new document/record in the database collection. There are several ways to create a document in mongoose. We will discuss them one by one.
create()
The create() method is a shortcut for creating a new document and saving it in the database. It accepts an object with the properties we want to set. It returns a promise that resolves to the created document.
const ashiq = await Person.create({ name: "Ashiq" }); // Here Person is the model
console.log(ashiq); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Ashiq', __v: 0 }
You may notice I used the await keyword before Person.create(). This is because create() returns a promise. We need to wait for the promise to resolve before we can use the created document. We can also use then() and catch() to handle the promise.
Person.create({ name: "Ashiq" })
.then((ashiq) => {
console.log(ashiq); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Ashiq', __v: 0 }
})
.catch((err) => {
console.log(err);
});
If you want to know more about promises, and async/await, read this article. Here, I explained how to handle asynchronous code in JavaScript.
_id and __v?_id
The _id is a unique identifier for each document. It is automatically added by mongoose to each document. It is a 12-byte value consisting of:
- 4-byte value representing the seconds since the Unix epoch,
- 3-byte machine identifier,
- 2-byte process id, and
- 3-byte counter, starting with a random value.
_id is in hexadecimal format, so 12 bytes = 96 bits = 24 hexadecimal digits. 2 hexadecimal digits = 1 byte.
Now, you might be wondering why we need a unique identifier for each document. The answer is simple. We need to be able to identify each document uniquely. For example, if we want to update a document, we need to know which document we want to update. We can do this by using the _id property.
__v
The __v is a property that is added by mongoose to keep track of the number of times a document has been modified. It is used for optimistic concurrency control.
I like this create() method, as it's very simple and easy to use. It saves documents in one line of code. It returns a promise that resolves and returns the created document, so we can use it in the next line of code.
save()
The save() method is used to save a new document to the database. It returns a promise that resolves to the created document. To use the save() method, we need to create a new document, and then use the save() method to save it to the database.
const muhammad = new Person({ name: "Muhammad" }); // First create a new document
const savedMuhammad = await muhammad.save(); // Then save it to the database
console.log(savedMuhammad); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Muhammad', __v: 0 }
You can also do it using chaining:
const muhammad = new Person({ name: "Muhammad" }).save(); // First create a new document and save it to the database
console.log(muhammad); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Muhammad', __v: 0 }
This method is useful when we want to create a new document and then add some properties to it before saving it. For example, we could create a new person and then add a photo URL:
const mother = new Person({ name: "Mother" });
mother.photosURLs.push("https://bit.ly/34Kvbsh");
await mother.save();
Once a document is created, the _id property is created, and it can't be changed. If we try to change it, we will get an error.
Now that we have our first two persons, we can create a new one with all properties filled, including parents:
const rizwan = await Person.create({
name: "Rizwan",
age: 25,
photosURLs: ["https://bit.ly/2Z4KKcF", "https://bit.ly/2Z4KKcF"],
mother: mother._id,
father: muhammad._id,
address: {
street: "Street 1",
city: "Rahim Yar Khan",
country: "Pakistan",
},
});
console.log(rizwan); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Rizwan', ... }
insertMany()
The insertMany() method is used to insert multiple documents into the database. It accepts an array of objects with the properties we want to set. It returns a promise that resolves to an array of the created documents.
const persons = await Person.insertMany([
{ name: "Person 1" },
{ name: "Person 2" },
{ name: "Person 3" },
]);
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }, ... ]
So, we have inserted persons into the database. Now, we can read them from the database. Let's see how we can do that.
Read Documents
To read documents from the database, we can use the following methods:
find()
Used to get documents from the database. It returns a promise that resolves to an array of documents. If no documents are found, it returns an empty array.
All Documents
If we want to get all documents from the database, we can use the find() method without any arguments:
const persons = await Person.find();
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }, ... ]
Filtered Documents
If we want to filter the documents we want to get, we can pass an object with the properties we want to filter by. For example, we can get all persons will country Pakistan:
const persons = await Person.find({ country: "Pakistan" });
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }, ... ]
We can use multiple filters to get more specific results. For example, we can get all persons with the country Pakistan and the city Rahim Yar Khan:
const persons = await Person.find({
country: "Pakistan",
city: "Rahim Yar Khan",
});
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }, ... ]
We can also filter documents using operators. For example, we can get all persons with age greater than 20:
const persons = await Person.find({
age: { $gt: 20 },
});
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }, ... ]
$gt is an operator that means greater than. mongoose provides a lot of operators that we can use to filter documents. For example:
| Operator | Description | Example | Result |
|---|---|---|---|
$gt | Greater than | { age: { $gt: 20 } } | All documents with age greater than 20 |
$lt | Less than | { age: { $lt: 20 } } | All documents with age less than 20 |
$gte | Greater than or equal to | { age: { $gte: 20 } } | All documents with age greater than or equal to 20 |
$lte | Less than or equal to | { age: { $lte: 20 } } | All documents with age less than or equal to 20 |
$ne | Not equal to | { age: { $ne: 20 } } | All documents with age not equal to 20 |
$in | In an array | { age: { $in: [20, 21] } } | All documents with age 20 or 21 |
$nin | Not in an array | { age: { $nin: [20, 21] } } | All documents with age not 20 or 21 |
$or | Logical or | { $or: [{ age: 20 }, { age: 21 }] } | All documents with age 20 or 21 |
$and | Logical and | { $and: [{ age: 20 }, { age: 21 }] } | All documents with age 20 and 21 |
$eq | Equal to | { age: { $eq: 20 } } | All documents with age equal to 20 |
$exists | Exists | { age: { $exists: true } } | All documents with age exists |
$type | Type of the field | { age: { $type: "number" } } | All documents with age of type number |
$regex | Regular expression | { name: { $regex: /person/i } } | All documents with name that matches the regular expression |
$mod | Modulus | { age: { $mod: [2, 0] } } | All documents with age that is divisible by 2 |
$all | All elements in an array | { age: { $all: [20, 21] } } | All documents with age 20 and 21 |
$elemMatch | At least one element in an array matches the condition | { age: { $elemMatch: { $gt: 20 } } } | All documents with age greater than 20 |
$size | Size of an array | { age: { $size: 2 } } | All documents with age of size 2 |
$where | Custom function | { $where: function() { return this.age > 20; } } | All documents with age greater than 20 |
const persons = await Person.find({
age: { $gt: 20, $lt: 30 },
});
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }, ... ]
Projection
We can also use the find() method to get only the properties we want, this is called projection. For example, we can get only the names of all persons:
const persons = await Person.find({}, { name: 1 });
The second argument of the find() method is an object with the properties we want to get. If we want to get all properties, we can pass an empty object. If we want to get only the name, we can pass { name: 1 }. If we want to get the name and the age, we can pass { name: 1, age: 1 }. If we want to get all properties except the name, we can pass { name: 0 }.
Sorting
We can also sort the documents using the find() method. For example, we can get all persons sorted by age in ascending order:
const persons = await Person.find().sort({ age: 1 });
Population
You see in the schema, father, and mother are of type ObjectId, with the ref property set to Person. So, when we get the persons, we can get the father and mother properties populated with the actual documents using the populate() method. For example, get all persons, and populate the father and mother properties:
const persons = await Person.find().populate("mother father");
console.log(persons);
The response will be like this:
[
{
"_id": "5f9e1b9b9c9c0b2b8c8b8b8b",
"name": "Person 1",
"age": 20,
"country": "Pakistan",
"city": "Rahim Yar Khan",
"father": {
"_id": "5f9e1b9b9c9c0b2b8c8b8b8c",
"name": "Person 2",
"age": 40,
"country": "Pakistan",
"city": "Rahim Yar Khan"
},
"mother": {
"_id": "5f9e1b9b9c9c0b2b8c8b8b8d",
"name": "Person 3",
"age": 35,
"country": "Pakistan",
"city": "Rahim Yar Khan"
}
}
]
See, how simple it is to get documents from the database using the find() method.
findOne()
The findOne() method is very similar to the find() method. You can do everything with the findOne() method that you can do with the find() method. The only difference is that the findOne() method returns only one document instead of an array of documents. It returns the first document. We can use the findOne() method like this:
const person = await Person.findOne();
console.log(person); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
Similar to the find() method, we can use the findOne() method to filter the documents we want to get. For example, we can get the first person with the country Pakistan:
const person = await Person.findOne({ country: "Pakistan" });
console.log(person); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
But just the difference is that the findOne() method returns only one document even if there are multiple documents that match the filter.
findById()
The findById() method is used to get a document by its _id. For example, we can get the document with the _id 5f9e1b9b9c9c0b2b8c8b8b8b:
const person = await Person.findById("5f9e1b9b9c9c0b2b8c8b8b8b");
console.log(person); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
I use this method a lot. It's very useful when we have a route with a parameter that is the _id of the document we want to get.
aggregate()
The aggregate() method is used to perform aggregation operations on the documents. We can use the aggregate() method to perform complex operations on the documents. For example, we can get the number of persons in the database:
const personsCount = await Person.aggregate([
{
$group: {
_id: null,
count: { $sum: 1 },
},
},
]);
console.log(personsCount); // [ { _id: null, count: 1 } ]
We can also use the aggregate() method to get the average age of all persons:
const personsAverageAge = await Person.aggregate([
{
$group: {
_id: null,
averageAge: { $avg: "$age" },
},
},
]);
console.log(personsAverageAge); // [ { _id: null, averageAge: 20 } ]
We can also use the aggregate() method to get the persons with an age greater than 20:
const persons = await Person.aggregate([
{
$match: {
age: { $gt: 20 },
},
},
]);
console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... } ]
I wrote a detailed article on the aggregate() method. You can read it
Update Documents
Now that we know how to create and read documents, we can also update them. For update, we have multiple methods. Let's see how we can use them.
save()updateOne()updateMany()replaceOne()findByIdAndUpdate()findOneAndUpdate()findOneAndReplace()
save()
It may look a bit strange to use the save() method to update a document. But it's not. The save() method is used to create a new document or update an existing document. Let's see how we can use it.
First, we need to create a new person:
const charles = new Person({
name: "Charles",
age: 25,
photosURLs: ["https://bit.ly/2QJCnMV"],
});
await charles.save();
Now, the charles variable has the new person. We can update the person by:
charles.photosURLs.push("https://bit.ly/2QJCnMV");
charles.age = 26;
await charles.save();
We can see that we can update the properties of the documents and then save them. If the document already exists on the database, mongoose will send an update command only with the fields changed, not the whole document.
There's another way to update a document. We can get the document from the database and then update it:
const ashiq = await Person.findOne({ name: "Ashiq" });
ashiq.photosURLs.push("https://bit.ly/2QJCnMV");
await ashiq.save();
updateOne()
updateOne() method is used to update only one document that matches the filter. Let's see how we can use it:
const rizwan = await Person.updateOne({ name: "Rizwan" }, { age: 27 });
console.log(rizwan); // { acknowledged: true, matchedCount: 1, modifiedCount: 1 }
It will update the first document that has the name Rizwan and set the age to 27, and will return a result object, that has the following properties:
acknowledged: a boolean indicating whether the server acknowledged the operationmatchedCount: the number of documents matched by the filter conditionmodifiedCount: the number of documents modified by the update operation
updateMany()
updateMany() is the same as updateOne(), but it updates all documents that match the filter. Let's see how we can use it:
const data = await Person.updateMany({ city: "Lahore" }, { age: 27 });
console.log(data); // { acknowledged: true, matchedCount: 2, modifiedCount: 2 }
It will update all documents that have the city Lahore and set the age to 27, and will return the same object as updateOne().
replaceOne()
replaceOne() method is used to replace a document with another document. Let's see how we can use it:
const data = await Person.replaceOne(
{ name: "Rizwan" },
{
name: "Rizwan",
age: 27,
city: "Sawat",
country: "Pakistan",
photosURLs: ["https://bit.ly/2QJCnMV"],
},
);
console.log(data); // { acknowledged: true, matchedCount: 1, modifiedCount: 1 }
It will replace the first document that has the name Rizwan with the new document. It will keep the _id of the old document 😊
findByIdAndUpdate()
findByIdAndUpdate() method is used to update a document by its _id. Let's see how we can use it:
const data = await Person.findByIdAndUpdate("5f9e1b9b9c9c0b2b8c8b8b8b", {
age: 27,
});
console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
It will update the document with the _id 5f9e1b9b9c9c0b2b8c8b8b8b and set the age to 27, and will return the old document.
findOneAndUpdate()
findOneAndUpdate() method is used to update the first document that matches the filter. Let's see how we can use it:
const data = await Person.findOneAndUpdate({ name: "Rizwan" }, { age: 27 });
console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
It will update the first document that has the name Rizwan and set the age to 27, and will return the old document.
findOneAndReplace()
findOneAndReplace() method is used to replace the first document that matches the filter. Let's see how we can use it:
const data = await Person.findOneAndReplace(
{ name: "Rizwan" },
{
name: "Rizwan",
age: 27,
city: "Sawat",
country: "Pakistan",
photosURLs: ["https://bit.ly/2QJCnMV"],
},
);
console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
It will replace the first document that has the name Rizwan with the new document. It will return the old document.
You notice that the findOneAnd*() and findByIdAnd*() methods return the document as it was before the update by default. findOneAndUpdate() is a single atomic operation on the server — the find and the update happen together, and the server can hand back either version of the document. If you want the updated document, use the new option:
const data = await Person.findOneAndUpdate(
{ name: "Rizwan" },
{ age: 27 },
{ new: true }, //This option will return the updated document
);
console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Rizwan', age: 27, ... }
Using { new: true } has no performance cost — there is no extra query involved; it simply tells the server which version of the document to return. By default, the new option is false because that matches the behavior of MongoDB's native findAndModify command.
Note that this only applies to the findOneAnd*()/findByIdAnd*() methods. updateOne() and updateMany() don't return a document at all — they return a result object with acknowledged, matchedCount, and modifiedCount.
So, there are many ways to update a document. Choose the one that fits your use case — findByIdAndUpdate() when you know the ID, updateOne() for a filter match, and updateMany() when you need to update multiple documents at once.
Now, let's see how we can delete documents.
Delete Documents
Deleting documents is similar to updating documents. We have many ways to delete documents, and we'll see them all.
deleteOne()deleteMany()findOneAndDelete()findByIdAndDelete()remove()(removed in Mongoose 7)
deleteOne()
The deleteOne() method is used to delete only one document that matches the filter. Let's see how we can use it:
const data = await Person.deleteOne({ name: "Rizwan" });
console.log(data); // { acknowledged: true, deletedCount: 1 }
It will delete the first document that has the name Rizwan.
deleteMany()
deleteMany() method is used to delete all documents that match the filter. Let's see how we can use it:
const data = await Person.deleteMany({ city: "Lahore" });
console.log(data); // { acknowledged: true, deletedCount: 2 }
If you execute deleteMany() with no filter, it will delete all documents in the collection.
const data = await Person.deleteMany();
console.log(data); // { acknowledged: true, deletedCount: 3 }
So, be careful when you use it.
findOneAndDelete()
Deletes the first document that matches the filter. Let's see how we can use it:
const data = await Person.findOneAndDelete({ name: "Rizwan" });
console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
findByIdAndDelete()
Deletes the document that matches the _id. Let's see how we can use it:
const data = await Person.findByIdAndDelete("5f9e1b9b9c9c0b2b8c8b8b8b");
console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }
remove()
The remove() method was deprecated for a long time and has been removed entirely in Mongoose 7. You may still see it in old codebases and tutorials, but don't use it in new code — use deleteOne() or deleteMany() instead.
const data = await Person.deleteMany({ city: "Lahore" });
console.log(data); // { acknowledged: true, deletedCount: 2 }
So, there are many ways to delete documents. You can choose the one that suits you.
Conclusion
In this doc, we learned how to create, read, update, and delete documents in MongoDB using Mongoose. We learned about the different ways to update and delete documents. We also learned about the deprecated methods.
We'll see more about Mongoose in the next docs.