Skip to main content

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:

info

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.

Creating a document using create()
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.

Creating a document using create()
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.

What is _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.

Source

__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.

Creating a document using save()
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:

Creating a document using save()
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();
caution

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:

Creating a document using create()
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.

Creating multiple documents using insertMany()
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:

Reading documents using find()
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:

Reading documents using find() with a country filter
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:

Reading documents using find with multiple filters
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:

Reading documents using find with an operator filter
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:

OperatorDescriptionExampleResult
$gtGreater than{ age: { $gt: 20 } }All documents with age greater than 20
$ltLess than{ age: { $lt: 20 } }All documents with age less than 20
$gteGreater than or equal to{ age: { $gte: 20 } }All documents with age greater than or equal to 20
$lteLess than or equal to{ age: { $lte: 20 } }All documents with age less than or equal to 20
$neNot equal to{ age: { $ne: 20 } }All documents with age not equal to 20
$inIn an array{ age: { $in: [20, 21] } }All documents with age 20 or 21
$ninNot in an array{ age: { $nin: [20, 21] } }All documents with age not 20 or 21
$orLogical or{ $or: [{ age: 20 }, { age: 21 }] }All documents with age 20 or 21
$andLogical and{ $and: [{ age: 20 }, { age: 21 }] }All documents with age 20 and 21
$eqEqual to{ age: { $eq: 20 } }All documents with age equal to 20
$existsExists{ age: { $exists: true } }All documents with age exists
$typeType of the field{ age: { $type: "number" } }All documents with age of type number
$regexRegular expression{ name: { $regex: /person/i } }All documents with name that matches the regular expression
$modModulus{ age: { $mod: [2, 0] } }All documents with age that is divisible by 2
$allAll elements in an array{ age: { $all: [20, 21] } }All documents with age 20 and 21
$elemMatchAt least one element in an array matches the condition{ age: { $elemMatch: { $gt: 20 } } }All documents with age greater than 20
$sizeSize of an array{ age: { $size: 2 } }All documents with age of size 2
$whereCustom function{ $where: function() { return this.age > 20; } }All documents with age greater than 20
Reading documents using find with multiple operator filters
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:

Reading documents using find with projection
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:

Reading documents using find with sorting
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:

Reading documents using find with the population
const persons = await Person.find().populate("mother father");

console.log(persons);

The response will be like this:

Response
[
{
"_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:

Reading a document using findOne
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:

Reading a document using findOne with a filter
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:

Reading a document using findById
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:

Reading documents using aggregate
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:

Reading documents using aggregate
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:

Reading documents using aggregate
const persons = await Person.aggregate([
{
$match: {
age: { $gt: 20 },
},
},
]);

console.log(persons); // [ { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... } ]
tip

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.

  1. save()
  2. updateOne()
  3. updateMany()
  4. replaceOne()
  5. findByIdAndUpdate()
  6. findOneAndUpdate()
  7. 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:

Creating a new document using save
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:

Updating a document using save
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:

Updating a document using save
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:

Updating a document using updateOne
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 operation
  • matchedCount: the number of documents matched by the filter condition
  • modifiedCount: 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:

Updating a document using updateMany
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:

Replacing a document using replaceOne
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:

Updating a document using findByIdAndUpdate
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:

Updating a document using findOneAndUpdate
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:

Replacing a document using findOneAndReplace
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.

tip

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:

Returning the new document using findOneAndUpdate
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.

  1. deleteOne()
  2. deleteMany()
  3. findOneAndDelete()
  4. findByIdAndDelete()
  5. 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:

Deleting a document using deleteOne with a filter
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:

Deleting multiple documents using deleteMany with a filter
const data = await Person.deleteMany({ city: "Lahore" });

console.log(data); // { acknowledged: true, deletedCount: 2 }
danger

If you execute deleteMany() with no filter, it will delete all documents in the collection.

Deleting all documents using deleteMany with no filter
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:

Deleting a document using findOneAndDelete with a filter
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:

Deleting a document using findByIdAndDelete with an id
const data = await Person.findByIdAndDelete("5f9e1b9b9c9c0b2b8c8b8b8b");

console.log(data); // { _id: 5f9e1b9b9c9c0b2b8c8b8b8b, name: 'Person 1', ... }

remove()

Removed in Mongoose 7

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.

Instead of remove(), use deleteMany()
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.