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

Array Updates with $, $[], $[identifier], arrayFilters, $push, $addToSet, and $pull

Mutate AtlasMart arrays precisely with positional operators, filtered array updates, bounded push patterns, duplicate-aware set semantics, and observable before/after evidence.

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

Learning outcomes

AtlasMart carts contain arrays of line-item subdocuments. The application must increment one SKU, normalize every line, update only low-price active items, add tags without accidental duplicates, append bounded audit history, and remove saved items. These operations look similar in JavaScript but have different binding rules and cardinality.

01

Distinguish the first-match positional $, all-elements $[], and filtered $[identifier] update operators.

02

Use arrayFilters to make the element predicate explicit and verify which elements changed.

03

Contrast $push with $addToSet and explain what “duplicate” means for scalar or document values.

04

Use $push modifiers to bound an embedded queue rather than allowing an array to grow forever.

05

Diagnose a positional update that has no valid array-element binding.

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:27038, 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-l2. Filtered positional array updates are still one-document writes. Their atomicity is at the containing document, not a guarantee that an independently stored product, inventory row, or order was changed with the cart.

1. Positional syntax answers “which elements?”

The positional $ placeholder targets the first array element that corresponds to the query condition. The all-positional $[] targets every element of the array. The filtered positional form $[identifier] targets every element satisfying a separately supplied arrayFilters predicate. Treat these as three different cardinality contracts rather than interchangeable syntax.

Form Targets Common mistake
items.$ First element bound by the query condition Using it without a query predicate that identifies an array element.
items.$[] All elements Applying a transformation intended for one subset to every line.
items.$[low] All elements matching arrayFilters Assuming the outer document filter also serves as the element filter.

The filtered identifier must be declared exactly once in arrayFilters. MongoDB also restricts certain operators inside array filters, including $expr, $text, and $where. Upsert with positional array operators has additional exact-equality requirements on the array field when the operation would insert a new document.

2. $push and $addToSet solve different problems

$push appends and therefore permits duplicates. That is appropriate for an ordered event history where two equal-looking events can still be distinct occurrences. $addToSet adds a value only if an equal value is not already present. It does not retroactively remove duplicates that already exist, and it does not sort the array. For document values, equality is structural; if your business concept of uniqueness is “one item per SKU” rather than exact BSON document equality, an embedded array is not automatically a uniqueness constraint.

Wrong design: “$addToSet makes my array a database set.”

It only suppresses insertion of an exactly equal value. Two item documents with the same SKU but different quantity or field order/shape can still be distinct BSON values. If SKU uniqueness is an invariant, model it explicitly—for example as one keyed subdocument per SKU, a separate collection with a unique index, or a single mutation that first matches the SKU and updates it.

3. Keep arrays bounded when growth is not the product

$push supports modifiers such as $each, $sort, and $slice. Together they can implement a bounded “latest N” embedded history. This is useful when only recent entries belong in the aggregate. It is not a substitute for a full audit log if older entries must remain durable and queryable.

4. Run the positional and array-mutation lab

bash · start the disposable MongoDB 8.3.8 lab
docker rm -f atlasmart-mongo-ch05-l2 2>/dev/null || truedocker run --name atlasmart-mongo-ch05-l2 -p 127.0.0.1:27038:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27038/atlasmart?directConnection=true" --quiet --eval 'printjson({version:db.version(), hello:db.hello().isWritablePrimary})' 
javascript · positional binding, filtered updates, duplicate behavior, bounded push, and pull
db.cart_mutations.drop();db.cart_mutations.insertOne({ _id:"cart-100", customerId:"cust-7", items:[  {sku:"cam-100",qty:1,priceCents:12500,state:"active"},  {sku:"battery-9",qty:2,priceCents:1900,state:"active"},  {sku:"strap-2",qty:1,priceCents:1200,state:"saved"} ], tags:["camera"], audit:[]});printjson({before:db.cart_mutations.findOne({_id:"cart-100"})});// $ updates the first array element bound by the query predicate.let r=db.cart_mutations.updateOne({_id:"cart-100","items.sku":"cam-100"},{$inc:{"items.$.qty":1}});printjson({step:"$ positional",matched:r.matchedCount,modified:r.modifiedCount,doc:db.cart_mutations.findOne({_id:"cart-100"})});// $[] updates every element.r=db.cart_mutations.updateOne({_id:"cart-100"},{$set:{"items.$[].checked":true}});printjson({step:"$[] all",matched:r.matchedCount,modified:r.modifiedCount,doc:db.cart_mutations.findOne({_id:"cart-100"})});// $[identifier] updates every element that satisfies arrayFilters.r=db.cart_mutations.updateOne({_id:"cart-100"},{$inc:{"items.$[low].qty":1}},{arrayFilters:[{"low.priceCents":{$lt:2000},"low.state":"active"}]});printjson({step:"$[low]",matched:r.matchedCount,modified:r.modifiedCount,doc:db.cart_mutations.findOne({_id:"cart-100"})});// $push can add duplicates; $addToSet prevents adding an equal value that already exists.db.cart_mutations.updateOne({_id:"cart-100"},{$push:{tags:"camera"}});db.cart_mutations.updateOne({_id:"cart-100"},{$addToSet:{tags:"camera"}});db.cart_mutations.updateOne({_id:"cart-100"},{$addToSet:{tags:"priority"}});printjson({step:"push-vs-addToSet",tags:db.cart_mutations.findOne({_id:"cart-100"}).tags});// Bounded queue pattern: append, sort newest first, keep three entries.for(let n=1;n<=4;n++){db.cart_mutations.updateOne({_id:"cart-100"},{$push:{audit:{$each:[{seq:n,event:"touch"}],$sort:{seq:-1},$slice:3}}})}printjson({step:"bounded-$push",audit:db.cart_mutations.findOne({_id:"cart-100"}).audit});r=db.cart_mutations.updateOne({_id:"cart-100"},{$pull:{items:{state:"saved"}}});printjson({step:"$pull",matched:r.matchedCount,modified:r.modifiedCount,after:db.cart_mutations.findOne({_id:"cart-100"})});// Wrong positional target: query does not bind items.sku, so positional $ cannot be used safely.try{db.cart_mutations.updateOne({_id:"cart-100"},{$inc:{"items.$.qty":1}})}catch(e){printjson({expectedError:e.codeName||e.name,message:e.message})}

Expected evidence

The camera quantity changes through $; every item gains checked:true through $[]; only active low-price items are incremented through the named filter; the first $push creates a duplicate camera tag while $addToSet refuses another exact duplicate; the audit array remains at three entries; and $pull removes every item whose embedded document matches the condition.

Verification checklist

  • The fixture has multiple items so element cardinality is observable.
  • Every mutation prints matched/modified counts or exact before/after state.
  • The named array filter contains both price and state conditions in one filter document for identifier low.
  • The duplicate-tag experiment demonstrates that $push and $addToSet are not synonyms.
  • The bounded audit queue never exceeds three elements.
  • The deliberately unbound positional update errors and leaves the document unchanged.

Check your understanding

  1. What makes items.$ choose an element?
  2. How does $[] differ from $[identifier]?
  3. $addToSet already found duplicate tags. Does it remove duplicates that existed before the operation?
  4. Why can an ever-growing embedded event array become a modeling problem?
  5. What is the atomicity boundary of these array changes?
Review the answers

The query predicate must bind a matching element in that array; $ is a placeholder for the first matching bound element.

$[] changes every array element; $[identifier] changes only elements satisfying the corresponding arrayFilters condition.

No. It only decides whether to add the requested value; it does not deduplicate the existing array.

The document grows in size and write/read cost, can approach the BSON document limit, and mixes independently retained history into the aggregate.

The containing MongoDB document. Other documents are not part of the same atomic write unless a transaction is deliberately used.

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

Lesson 3 shifts from element selection to document creation: what exactly MongoDB inserts when an update uses upsert:true, and why concurrency must be constrained by an actual uniqueness rule.

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.