Upserts, findOneAndUpdate and Bulk Writes

Intermediate
12 min

Upserts, findOneAndUpdate and Bulk Writes

The basic insertOne, updateOne and deleteOne calls each do one thing. Production code often needs combinations: "create this record unless it exists, then update it", "update and give me the result in the same call", or "apply these two thousand changes without two thousand round trips". After this lesson you will be able to write upserts correctly, use the findOneAnd* family for atomic read-and-modify logic, update with aggregation pipelines, and batch writes with bulkWrite().

Upsert: Update or Insert in One Call

Add { upsert: true } to any update and MongoDB inserts a new document when the filter matches nothing. The new document is built from the equality conditions of the filter plus the update operators.

javascript
db.visits.updateOne( { page: "/pricing", day: ISODate("2026-03-01") }, { $inc: { hits: 1 }, $setOnInsert: { firstSeen: new Date() } }, { upsert: true } ) // { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedId: ObjectId('...') }

$setOnInsert sets fields only when the upsert inserts; on later calls it is ignored. upsertedId in the result tells you an insert happened.

Two concurrent upserts with the same filter can both see "no match" and both insert. Protect upsert keys with a unique index; the loser then receives a duplicate key error (E11000) that your code can retry as a plain update.

Read and Modify Atomically: findOneAndUpdate and Friends

updateOne returns counts, not data. The findOneAnd* methods return the affected document, and the read and the write happen as one atomic operation on the server:

| Method | Does | |---|---| | findOneAndUpdate(filter, update, opts) | applies update operators or a pipeline, returns the document | | findOneAndReplace(filter, doc, opts) | swaps the whole document (except _id) | | findOneAndDelete(filter, opts) | removes and returns the document |

Useful options: returnDocument: "after" (the default returns the pre-update document), sort to choose which match to take, projection, and upsert.

javascript
// Claim the oldest pending job so no other worker can take it const job = db.jobs.findOneAndUpdate( { status: "pending" }, { $set: { status: "running", worker: "w-7", startedAt: new Date() } }, { sort: { createdAt: 1 }, returnDocument: "after" } )

Because the match and the update are atomic, two workers can never claim the same job.

Replacing a Whole Document

replaceOne(filter, replacement, { upsert }) overwrites everything except _id. Use it only when the application holds the complete current document: any field absent from the replacement is deleted.

Updates Written as Aggregation Pipelines

A plain update cannot reference other fields of the same document. Passing an array instead of an update document runs a pipeline restricted to $set/$addFields, $unset/$project and $replaceRoot/$replaceWith:

javascript
db.orders.updateMany( { total: { $exists: false } }, [ { $set: { total: { $multiply: ["$price", "$qty"] }, updatedAt: "$$NOW" } }, { $unset: "legacyTotal" } ] )

Use this to backfill computed fields or copy one field into another without round-tripping documents through the client.

Bulk Writes

bulkWrite() sends a list of heterogeneous operations in one request:

javascript
const result = db.products.bulkWrite([ { insertOne: { document: { sku: "LAMP-2", price: 25 } } }, { updateOne: { filter: { sku: "DESK-1" }, update: { $inc: { stock: -1 } } } }, { replaceOne: { filter: { sku: "CHAIR-3" }, replacement: { sku: "CHAIR-3", price: 99 }, upsert: true } }, { deleteOne: { filter: { sku: "OLD-9" } } } ], { ordered: false }) result.insertedCount // 1 result.modifiedCount // depends on matches result.upsertedCount // 1 if CHAIR-3 was missing

ordered: true (the default) executes operations serially and stops at the first error; ordered: false lets the server continue, reorder and parallelise, and reports every failure together in writeErrors. Unordered is faster for imports and idempotent updates; ordered is right when later operations depend on earlier ones. The same ordered option exists on insertMany().

Common Mistakes

  • Passing a plain document to updateOne. Updates require operators ($set, ...) or a pipeline; the server rejects { name: "x" } with "Update document requires atomic operators".
  • Assuming findOneAndUpdate returns the new document. It returns the old one unless returnDocument: "after" is set.
  • Using replaceOne for partial edits, silently deleting fields not present in the replacement.
Quick Quiz
Question 1 of 3

In an upsert, when does `$setOnInsert` take effect?

Key Takeaways

  • { upsert: true } inserts when nothing matches; $setOnInsert sets creation-only fields; a unique index prevents duplicate upserts.
  • findOneAndUpdate, findOneAndReplace and findOneAndDelete combine a read and a write atomically and return the document; use returnDocument: "after" for the new version.
  • replaceOne swaps a whole document; use it only when you hold the complete record.
  • Pipeline updates ([ { $set: ... } ]) can reference other fields and use expressions.
  • bulkWrite() batches mixed operations in one request; choose ordered: false for speed when operations are independent.

Next lesson: Deleting Documents — remove data with deleteOne() and deleteMany(), and learn when dropping a collection is the better choice.

Upserts, findOneAndUpdate and Bulk Writes - MongoDB | CodeYourCraft | CodeYourCraft