Chapter 05 · Updates, Array Mutations, Upserts, findAndModify, and Bulk Writes
$set, $unset, $inc, $mul, $min, $max, $rename, and Replacement Updates
Use MongoDB update operators and replacement writes safely by reading acknowledged result metadata, comparing exact before/after state, and recognizing when a local field mutation is safer than whole-document replacement.
Learning outcomes
AtlasMart needs to change product state without accidentally erasing unrelated fields or misreading a successful no-op as a failed match. MongoDB exposes two distinct mutation styles: operator updates modify selected paths, while replacement writes replace the document body. The difference is architectural, not cosmetic: the first preserves unspecified fields; the second intentionally discards them.
Explain what $set, $unset, $inc, $mul, $min, $max, and $rename do to an existing document and what they do when a field is absent or incompatible.
Interpret acknowledged, matchedCount, and modifiedCount without conflating no match with a matched no-op.
Contrast updateOne() operator documents with replaceOne() replacement documents and the immutable _id boundary.
Demonstrate a type error and a destructive replacement mistake, then verify the repaired state.
Choose path-level updates versus whole-document replacement from ownership and schema-contract reasoning.
Mandatory examples use MongoDB Community Server
8.3.8 in the pinned
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
image, a disposable standalone mongod published
only on 127.0.0.1:27037, and mongosh 2.10.0.
PyMongo examples target the 4.17 line where driver behavior
matters. Authentication and TLS are intentionally disabled
only inside this isolated loopback lab; do not copy that
posture to a shared or remotely reachable server. Default
standalone read/write concern semantics are used, no
replica-set/sharding guarantees are claimed, and the reset
path removes atlasmart-mongo-ch05-l1. The lesson
uses one document at a time, so the write itself is atomic at
the MongoDB document boundary; it does not protect invariants
that span multiple documents.
1. Update operators express intent at field-path granularity
$set assigns a value and creates the field if the
path can be created. $unset removes a field.
Numeric operators such as $inc and
$mul compute a new value from the current value,
while $min and $max conditionally
replace only when the proposed value compares lower or higher.
$rename moves a field name. These are server-side
mutations: the client does not need to fetch the document,
calculate a new copy, and race another writer.
| Operator | AtlasMart use | Important boundary |
|---|---|---|
| $set | Change status or nested campaign metadata | Can create a missing field; setting the same value can yield matched=1, modified=0. |
| $unset | Remove obsolete optional metadata | Removing a missing field is a no-op. |
| $inc / $mul | Adjust counters, stock, or numeric factors | $inc on an explicit null value errors; type contracts matter. |
| $min / $max | Keep a floor/ceiling or extreme observed value | Comparison follows BSON comparison semantics; do not mix incompatible domain types casually. |
| $rename | Migrate a field name | It is a data mutation, not an application alias; readers must tolerate rollout order. |
2. Acknowledged results describe what MongoDB observed
An acknowledged update result separates at least two questions.
matchedCount asks whether the filter selected a
document. modifiedCount asks whether the
persisted document changed. A matched no-op commonly returns
matchedCount: 1 and modifiedCount: 0.
A miss returns both as zero. Production code should decide which
distinction matters to the business rule instead of treating
every zero modification as an error.
Each single-document update is atomic for that document. That means concurrent readers do not observe a half-applied combination of operators on the same document. It does not make a read-then-update sequence atomic, and it does not enforce cross-document inventory/payment invariants.
3. Replacement is deliberately destructive
updateOne() expects update operators or an allowed
aggregation update pipeline. To replace the document body, use
replaceOne(). The replacement cannot contain update
operators. On a matched replacement, MongoDB preserves the
immutable _id value when it is omitted; if you
include _id, it must equal the existing value.
Every other omitted field disappears. That makes replacement
appropriate when one service owns the entire document
representation and dangerous when multiple writers own different
fields.
Reading a product, constructing a partial object such as
{name, priceCents}, and passing it to
replaceOne() silently removes stock, category,
metrics, schema-version, and any other omitted fields. The
repair is either a complete replacement built from an explicit
canonical model or a narrow operator update that changes only
the paths this writer owns.
4. Run the operator-versus-replacement lab
docker rm -f atlasmart-mongo-ch05-l1 2>/dev/null || truedocker run --name atlasmart-mongo-ch05-l1 -p 127.0.0.1:27037:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27037/atlasmart?directConnection=true" --quiet --eval 'printjson({version:db.version(), hello:db.hello().isWritablePrimary})'
db.product_mutations.drop();db.product_mutations.insertOne({ _id:"sku-cam-100", name:"AtlasCam 100", priceCents:12500, stock:12, reorderPoint:5, metrics:{views:100, purchases:4}, status:"active", legacyCategory:"cameras", optionalNote:null});const before=db.product_mutations.findOne({_id:"sku-cam-100"}); printjson({before});let r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$set:{"metrics.lastCampaign":"fall-2026",status:"featured"}});printjson({step:"$set",acknowledged:r.acknowledged,matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$unset:{optionalNote:""},$inc:{stock:-2,"metrics.purchases":1}});printjson({step:"$unset+$inc",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$mul:{priceCents:1.1},$min:{reorderPoint:4},$max:{"metrics.views":120}});printjson({step:"$mul+$min+$max",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$rename:{legacyCategory:"category"}});printjson({step:"$rename",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});// No-op is different from no match: matchedCount 1, modifiedCount 0.r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$set:{status:"featured"}});printjson({step:"no-op",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount});// Replacement rewrites every field except immutable _id. Omitting category here really removes it.r=db.product_mutations.replaceOne({_id:"sku-cam-100"},{name:"AtlasCam 100",priceCents:13750,stock:10,status:"featured",schemaVersion:2});printjson({step:"replaceOne",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,after:db.product_mutations.findOne({_id:"sku-cam-100"})});// Deliberate type error: $inc on null is invalid.db.product_mutations.insertOne({_id:"bad-null",counter:null});try{db.product_mutations.updateOne({_id:"bad-null"},{$inc:{counter:1}})}catch(e){printjson({expectedError:e.codeName||e.name,message:e.message})}
from pymongo import MongoClientclient=MongoClient("mongodb://127.0.0.1:27037/?directConnection=true", serverSelectionTimeoutMS=3000)coll=client.atlasmart.product_mutationsres=coll.update_one({"_id":"sku-cam-100"},{"$set":{"status":"featured"}})print({"acknowledged":res.acknowledged,"matched":res.matched_count,"modified":res.modified_count})client.close()
Expected evidence
You should observe each update returning acknowledged result
metadata, the same _id across the replacement, the
no-op reporting one match and zero modifications, and the
explicit null counter rejecting $inc.
Exact error text can vary by server/driver version, so verify
the error category and unchanged document rather than matching a
brittle string.
Verification checklist
- The container is bound only to loopback on port 27037.
- Each update prints both result metadata and a fresh server-side document read.
- The no-op is distinguishable from a missing document.
- The replacement visibly removes fields not included in its replacement body.
- The null-counter example errors without mutating the field.
- Cleanup removes the disposable server.
Check your understanding
- Why can matchedCount be 1 while modifiedCount is 0?
- Why is replaceOne riskier than $set for a shared document?
- Does $inc protect inventory from every oversell race?
- What does an acknowledged result prove?
- When is replacement appropriate?
Review the answers
The filter found a document, but the requested mutation produced the same stored value or otherwise made no change.
Replacement discards every non-_id field omitted from the replacement body, so another writer's fields can be erased.
No. It is atomic within one document, but the filter must encode the business precondition, and cross-document invariants need a stronger design.
It proves the server acknowledged the operation under the configured write concern and reports match/change metadata; it does not by itself prove a higher-level business invariant.
When one writer owns the full canonical document representation and intentionally wants unspecified fields removed.
docker rm -f atlasmart-mongo-ch05-l1
The next lesson keeps single-document atomicity but moves the mutation target inside arrays, where positional binding and duplicate semantics create new failure modes.
Authoritative references
- MongoDB release notes — Current stable server series and patch history.
- MongoDB 8.3 release notes — 8.3.8 is the latest released 8.3 patch at review time; 8.3.9 is upcoming.
- mongosh release notes — mongosh 2.10.0 was released August 13, 2026.
- PyMongo release notes — Current PyMongo 4.17 line and driver changes.
- MongoDB update operators — Official definitions for $set, $unset, $inc, $mul, $min, $max, $rename, and related operators.
- $inc update operator — Documents missing-field creation, null-value errors, and single-document atomic behavior.
- updateOne() — Official update syntax, result behavior, upsert option, and update-pipeline boundary.
- replaceOne() — Replacement semantics, immutable _id behavior, and upsert boundary.