Chapter 05 · Updates, Array Mutations, Upserts, findAndModify, and Bulk Writes

Upserts, Equality Predicates, Generated Documents, and Race-Safe Expectations

Understand how MongoDB constructs upserted documents, why equality predicates and unique indexes matter, and how to design idempotent race-safe create-or-update paths.

Intermediate90–120 minutesMutation semantics + failure-aware labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17Last reviewed: September 2026

Learning outcomes

AtlasMart wants “create inventory if absent, otherwise update it” without a separate existence check. That is an upsert: one update request that takes an insert path only when no document matches. The subtle part is not the option name; it is understanding how the inserted document is constructed and how concurrent callers are prevented from creating two logical records.

01

Explain the update-versus-insert branches of upsert and interpret upsertedId separately from matched/modified counts.

02

Identify which equality predicates can contribute literal fields to the inserted document and which filter expressions are only selection logic.

03

Use $setOnInsert for fields that should be initialized only on the insert branch.

04

Explain why a unique index is the database-level protection against duplicate logical identities under concurrent upserts.

05

Design idempotent create-or-update operations without a read-before-write race.

Reproducible lab baseline · reviewed 2 September 2026

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:27039, 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-l3. The lab adds a unique compound index on (tenantId, sku) because race-safe expectations require a server-enforced identity, not application timing.

1. Upsert is one conditional write, not “find then insert”

With upsert:false, an unmatched update simply reports zero matches. With upsert:true, MongoDB instead creates a new document. The inserted document is derived from equality conditions in the filter plus the update operations. An operator such as $gte is a predicate, not a literal value to copy into the document. Therefore, design filters so their role is clear: identity equality belongs in the filter; mutable values normally belong in $set, $inc, or other update operators.

$setOnInsert is especially useful for creation metadata such as createdAt, schemaVersion, or initial ownership fields. It has no effect on the update branch when a document already exists.

2. Result metadata tells you which branch happened

Outcome matchedCount modifiedCount upsertedId
Existing document changed 1 usually 1 none/null
Existing document matched but no-op 1 0 none/null
No match; insert branch 0 0 new _id

Applications often care about branch identity—for example, whether to emit a “created” event. Do not infer insert versus update from modifiedCount; an insert and a matched no-op can both have zero modifications for different reasons.

3. Race safety comes from uniqueness, not optimism

Two processes can issue the same upsert at nearly the same time. If the logical identity is not backed by a unique index, both operations can observe no matching document and create duplicate logical records. MongoDB’s own guidance is to uniquely index the upsert filter fields when duplicate upserts must be prevented. With a unique index, the database becomes the arbiter: one insert wins and a conflicting insert attempt receives a duplicate-key error that the application can classify and, where appropriate, retry as a normal update/read path.

Deliberately wrong pattern: findOne() then insertOne()

The gap between the read and insert is a race window. “It usually works” under low concurrency is not a correctness proof. Use one upsert operation and enforce the identity with a unique index.

4. Run the upsert construction and race-safety lab

bash · start the disposable MongoDB 8.3.8 lab
docker rm -f atlasmart-mongo-ch05-l3 2>/dev/null || truedocker run --name atlasmart-mongo-ch05-l3 -p 127.0.0.1:27039:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27039/atlasmart?directConnection=true" --quiet --eval 'printjson({version:db.version(), hello:db.hello().isWritablePrimary})' 
javascript · insert branch, update branch, predicate construction, and unique identity
db.inventory_upserts.drop();db.inventory_upserts.createIndex({tenantId:1,sku:1},{unique:true});// Insert path: equality predicates contribute fields; $setOnInsert only applies now.let r=db.inventory_upserts.updateOne( {tenantId:"t1",sku:"cam-100"}, {$set:{onHand:5,updatedAt:ISODate("2026-09-02T08:00:00Z")},$setOnInsert:{createdBy:"seed",schemaVersion:1}}, {upsert:true});printjson({step:"insert-upsert",matched:r.matchedCount,modified:r.modifiedCount,upsertedId:r.upsertedId,doc:db.inventory_upserts.findOne({tenantId:"t1",sku:"cam-100"})});// Update path: $setOnInsert no longer changes the existing document.r=db.inventory_upserts.updateOne( {tenantId:"t1",sku:"cam-100"}, {$inc:{onHand:2},$set:{updatedAt:ISODate("2026-09-02T08:05:00Z")},$setOnInsert:{createdBy:"SHOULD-NOT-REPLACE"}}, {upsert:true});printjson({step:"existing-upsert",matched:r.matchedCount,modified:r.modifiedCount,upsertedId:r.upsertedId,doc:db.inventory_upserts.findOne({tenantId:"t1",sku:"cam-100"})});// A range predicate is selection logic, not a literal field to materialize.r=db.inventory_upserts.updateOne( {tenantId:"t1",sku:"battery-9",onHand:{$gte:0}}, {$set:{onHand:3},$setOnInsert:{createdBy:"seed"}}, {upsert:true});printjson({step:"range-filter-upsert",upsertedId:r.upsertedId,doc:db.inventory_upserts.findOne({tenantId:"t1",sku:"battery-9"})});// Unique index is the race arbiter. A second logical identity cannot be inserted.try{db.inventory_upserts.insertOne({tenantId:"t1",sku:"cam-100",onHand:99})}catch(e){printjson({expectedDuplicate:e.codeName||e.name,keyPattern:e.keyPattern,keyValue:e.keyValue})}
python · driver-side create-or-update with ReturnDocument.AFTER
from pymongo import MongoClient, ReturnDocumentfrom pymongo.errors import DuplicateKeyErrorclient=MongoClient("mongodb://127.0.0.1:27039/?directConnection=true")coll=client.atlasmart.inventory_upserts# Two callers can safely converge on one identity only if the identity is enforced uniquely.for delta in (1, 1):    doc=coll.find_one_and_update(        {"tenantId":"t2","sku":"strap-2"},        {"$inc":{"onHand":delta},"$setOnInsert":{"schemaVersion":1}},        upsert=True,        return_document=ReturnDocument.AFTER,    )    print(doc)client.close()

Expected evidence

The first camera write reports an upsertedId and includes identity fields plus update-set values. The second reports a match and leaves createdBy unchanged because $setOnInsert is inactive. The range predicate does not materialize a document field containing { $gte: 0 }. The explicit duplicate insert fails because the compound unique index protects one document per tenant/SKU.

Verification checklist

  • The unique index exists before the upsert race discussion.
  • Insert and update branches are distinguished through result metadata.
  • $setOnInsert is visibly active only during insertion.
  • The range predicate is not confused with inserted literal state.
  • The duplicate-key example demonstrates the server-enforced invariant.
  • No client-side “check then insert” is required.

Check your understanding

  1. Why is upsert safer than find-then-insert?
  2. Does upsert alone guarantee one logical document per SKU?
  3. When does $setOnInsert run?
  4. How do you tell that an upsert inserted?
  5. Why should mutable state usually not appear as broad equality in the identity filter?
Review the answers

It collapses selection and mutation into one server operation, removing the explicit application race window between a separate read and insert.

No. If concurrent operations can create the same logical identity, enforce that identity with a unique index.

Only when the upsert takes the insertion path; it does not overwrite fields when an existing document is updated.

Use upsertedId/upserted_count or equivalent result metadata, not modifiedCount.

Because it can cause an existing logical entity not to match and thereby turn an intended update into a competing insert path.

bash · cleanup/reset
docker rm -f atlasmart-mongo-ch05-l3

Lesson 4 now needs the document itself as the return value, not just counts: MongoDB’s find-and-modify family combines selection, mutation, and before/after return semantics in one operation.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.