CRUD and querying
Update and delete deliberately
Use update operators, upserts, single-document methods, and guarded deletions without replacing or removing too much data.
9 minute lesson
MongoDB update methods take two documents: a filter that selects, and an update document built from operators such as $set, $inc, and $push. Keeping them separate in your head prevents the two classic accidents: updating the wrong documents, and replacing a document when you meant to change one field.
db.animals.updateOne(
{ name: 'Roger' },
{ $inc: { visits: 1 }, $push: { favoriteFoods: 'cheese' } }
)
// { acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
$inc increments a number, $push appends to an array, $set assigns a value, $unset removes a field. Use updateOne() when one match is the intended boundary and updateMany() when every matching document should change.
Always read the response. matchedCount: 0 means your filter found nothing and the update did no work — no error is raised. A misspelled name or a string passed where an ObjectId was needed fails silently this way.
If you pass a plain document without operators to replaceOne(), the entire document is replaced except _id. That is occasionally what you want. Reaching for it by habit deletes every field you forgot to include.
An upsert inserts the document when the filter matches nothing:
db.counters.updateOne(
{ _id: 'pageviews' },
{ $inc: { total: 1 } },
{ upsert: true }
)
Deleting with a guard
Deletion follows the same filter logic:
db.animals.deleteOne({ _id: ObjectId('688f4a2e9d1c2a7b3e4f5a60') })
// { acknowledged: true, deletedCount: 1 }
An empty filter matches every document. That makes deleteMany({}) useful for wiping a disposable test collection and catastrophic against a real one. There is no confirmation prompt and no undo.
Reject an empty filter when it is built from user input or optional parameters:
if (Object.keys(filter).length === 0) {
throw new Error('Refusing to delete with an empty filter')
}
const result = await animals.deleteMany(filter)
deleteMany({}) removes the documents but keeps the collection and its indexes. drop() removes the collection and its indexes too. Use drop() only when that is the result you intend.
My advice is to preview every broad mutation before running it. Count what the filter matches first:
db.animals.countDocuments({ species: 'hamster' })
// 2 ← expected? then delete
db.animals.deleteMany({ species: 'hamster' })
Test the non-matching case too: run a delete with a filter you know matches nothing and confirm deletedCount: 0. Code that treats “zero deleted” as success hides typos in production filters.
Lesson completed