BlogNode.js

MongoDB findOneAndUpdate vs updateOne

Written by Codemzy on September 26th, 2023

There's more than one way to update a document in MongoDB. Here's when (and why) I use `findOneAndUpdate` instead of `updateOne` to update a document with MongoDB in Node.js.

I've always used updateOne for updating individual documents in my MongoDB databases.

And I still do - when I just need a simple update.

But lately, I've found myself using findOneAndUpdate much more. Especially when building an API where I need to return the updated content from the back-end to the front-end client.

So let's look at the differences between findOneAndUpdate and updateOne - and when you would use them.

updateOne

  • Updates a document
  • Can insert a document (with upsert)

updateOne updates a document in MongoDB. It can also insert a document with the upsert: true option.

You can update a single document using the collection.updateOne() method. The updateOne() method accepts a filter document and an update document.

- MongoDB Update a Document

If you just need to update a document in MongoDB, this is the command you will want to use.

await db.collection('users').updateOne({ user: userId }, {
  $set: { name: "codemzy" },
});

🙏 Please, please, please, remember to use $set or one of the other field update operators - if you forget, your update could override the entire document (not just the field(s) you want to update!).

But what if you want to update a document and then return the updated document to the user? Do you need to run a findOne command before the update?

Or if you want to know what a field value was before the update? Do you need to run a findOne command after the update?

Noooo!

For either of these use cases, you need findOneAndUpdate.

findOneAndUpdate

  • Updates a document
  • Can insert a document (with upsert)
  • Returns the document either before or after the update

findOneAndUpdate does everything that updateOne does, plus returns the document (either before or after the update depending on the returnDocument option.

By default returnDocument is "before", which means the document is returned before the update. I guess this makes sense, since the command is findOneAndUpdate, not updateAndFindOne.

So it finds the document first, and then updates.

But you can also get the updated document returned, by setting returnDocument to "after", like this:

let result = await db.collection('users').findOneAndUpdate({ user: userId }, {
  $set: { name: "codemzy" },
}, { returnDocument: "after" });

console.log(result.value.name);
// "codemzy"

Let's look at a couple more use cases, and how findOneAndUpdate can handle them.

Knowing a field value before an update

Sometimes, you might need to update a document but still know what the value was before.

For example, we might want to log what the update was.

Let's imagine a user is changing the price of a product. We might want to log what the price was before the change so that we have a record for any customer queries.

Instead of doing a findOne to get the value and then doing the update, we can use findOneAndUpdate to do this in one call to the database. Pretty cool!

By default, findOneAndUpdate returns the document before the update, so this works out of the box.

let result = await db.collection('users').findOneAndUpdate({ product: productId }, {
  $set: { price: newPrice },
}, { returnDocument: "after" });
if (result.value) {
  console.log(`Pricing of product ${productId} changed from ${result.value.price} to ${newPrice}.`);
}

Another good trick if you are using the upsert option, is that result.value will be null if there was nothing to update and the command created (upserted) a new document. This is how you can know if findOneAndUpdate upserted or updated.

Getting a document after an update

Sometimes, you might need to update a field in a document, but return the entire document (or a different field value) after the update.

For example, let's imagine you use the same command to perform all user updates, and conditionally add properties to an updates object.

let updates = {
  ...(newEmail && {email: newEmail}),
  ...(newName && {name: newName}),
  ...(newAddress && {address: newAddress}),
};

When you run your findOneAndUpdate, you don't know which fields have been updated. Maybe this time the user updated just their name, or their name and their email, or any other combination of updates.

Instead of figuring all that out, we can use findOneAndUpdate to return the document after the update.

let result = await db.collection('users').findOneAndUpdate({ user: userId }, {
  $set: updates,
}, { returnDocument: "after" });

Now we know that result.value contains the latest version of the user, with all of the updates. We can return that data to the front-end, or email the user with a confirmation, any we know we have the correct up-to-date information.


I didn't cover it much in this blog post, but if you want to learn more about the upsert option, I have a blog post on MongoDB findOneAndUpdate and how to know if it's an upsert.