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().
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.
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.
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.
// 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.
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.
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:
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.
bulkWrite() sends a list of heterogeneous operations in one request:
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 missingordered: 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().
updateOne. Updates require operators ($set, ...) or a pipeline; the server rejects { name: "x" } with "Update document requires atomic operators".findOneAndUpdate returns the new document. It returns the old one unless returnDocument: "after" is set.replaceOne for partial edits, silently deleting fields not present in the replacement.In an upsert, when does `$setOnInsert` take effect?
{ 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.[ { $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.