35 CRUD: Update – Präzise Modifikation statt Überschreibung

Update-Operationen sind fundamentaler Teil jeder Datenbank-Anwendung. User ändert sein Passwort, ein Product läuft aus dem Stock, ein Counter wird inkrementiert, ein Tag wird zu einer Liste hinzugefügt – all dies sind Updates. Im Gegensatz zu relationalen Datenbanken, wo Updates typischerweise komplette Rows überschreiben, erlaubt MongoDB granulare, Feld-Level-Modifikationen. Man kann ein einzelnes nested Field ändern, ein Array-Element hinzufügen oder einen numerischen Wert inkrementieren – alles ohne das gesamte Dokument zu re-schreiben.

Diese Granularität ist nicht nur Convenience, sondern Performance-kritisch. Ein 10-KB-Dokument mit einem 1-Byte-Counter zu überschreiben, nur um den Counter zu inkrementieren, ist verschwenderisch. MongoDB’s Update-Operatoren erlauben atomare, in-place Modifications. Aber diese Macht kommt mit Komplexität – dutzende Operatoren mit subtilen Semantiken, Array-Update-Patterns mit $-Positional-Operatoren, Concurrency-Issues bei konkurrierenden Updates. Dieses Kapitel navigiert durch die Update-Landschaft systematisch, mit Fokus auf praktische Patterns und häufige Pitfalls.

35.1 updateOne und updateMany: Scoping der Änderungen

MongoDB bietet zwei primäre Update-Methoden, die sich darin unterscheiden, wie viele Dokumente sie betreffen.

updateOne() – Modifiziert das erste Match:

db.users.updateOne(
  { username: "alice" },                     // Filter
  { $set: { lastLogin: new Date() } }        // Update
)

Dies findet das erste Dokument mit username: "alice" und updated es. Wenn multiple Dokumente matchen, wird nur das erste (gemäß natural order oder Index-Order) updated. Die Response zeigt, was passiert ist:

{
  acknowledged: true,
  matchedCount: 1,    // 1 Dokument matched den Filter
  modifiedCount: 1,   // 1 Dokument wurde modified
  upsertedId: null    // Kein Upsert passiert
}

matchedCount vs. modifiedCount ist eine subtile aber wichtige Unterscheidung. Wenn das Update nichts ändert (etwa $set auf einen Wert, den das Feld bereits hat), ist matchedCount: 1 aber modifiedCount: 0. Dies kann relevant sein für Idempotenz-Checks.

updateMany() – Modifiziert alle Matches:

db.users.updateMany(
  { status: "inactive" },
  { $set: { archived: true, archivedAt: new Date() } }
)

Dies findet alle Dokumente mit status: "inactive" und updated sie alle. Die Response zeigt totale Counts:

{
  acknowledged: true,
  matchedCount: 1532,
  modifiedCount: 1532
}

Für Bulk-Operations – etwa “Archive all inactive users” oder “Set default value for all documents missing a field” – ist updateMany() essentiell. Aber Vorsicht: Ein falsch geschriebener Filter kann unbeabsichtigt tausende Dokumente updaten.

Safety-Pattern – Dry-Run via countDocuments:

Bevor man ein großes updateMany() ausführt, sollte man prüfen, wie viele Dokumente betroffen sind:

// Check wie viele matched werden
const affectedCount = db.users.countDocuments({ status: "inactive" })
print(`Would update ${affectedCount} documents`)

// User-Confirmation in Script
if (affectedCount > 1000) {
  print("WARNING: Large update! Confirm before proceeding.")
}

// Dann actual update
db.users.updateMany({ status: "inactive" }, { $set: { archived: true } })

Dies verhindert versehentliche Mass-Updates bei Typos im Filter.

35.2 $set und $unset: Felder setzen und entfernen

Die fundamentalsten Update-Operatoren sind $set (Feld setzen oder überschreiben) und $unset (Feld entfernen).

$set – Feld setzen oder erstellen:

db.users.updateOne(
  { username: "alice" },
  { $set: { 
      email: "alice@newdomain.com",
      updatedAt: new Date()
  }}
)

Wenn email bereits existiert, wird es überschrieben. Wenn nicht, wird es erstellt. $set ist idempotent – mehrmaliges Ausführen mit demselben Wert ändert nichts (außer modifiedCount bleibt 0 nach dem ersten).

$set auf nested Fields:

db.users.updateOne(
  { username: "alice" },
  { $set: { "profile.bio": "Coffee lover and developer" } }
)

Dot-Notation erlaubt Updates auf tief verschachtelte Felder ohne das gesamte Parent-Object zu überschreiben. Wenn profile nicht existiert, wird es erstellt. Wenn profile existiert aber bio nicht, wird bio hinzugefügt.

$unset – Feld komplett entfernen:

db.users.updateOne(
  { username: "alice" },
  { $unset: { temporaryToken: "" } }
)

Der Value zu $unset ist ignoriert (oft "" oder 1 per Convention) – es geht nur um das Key. Das Feld wird komplett aus dem Dokument entfernt, nicht auf null gesetzt. Dies ist wichtig für Storage-Efficiency und Schema-Clarity.

$set vs. $unset vs. null:

Drei Wege, ein “kein Wert”-Zustand zu repräsentieren:

// Option 1: Field existiert mit null
{ username: "alice", nickname: null }

// Option 2: Field fehlt komplett
{ username: "alice" }

// Option 3: Field existiert mit empty string
{ username: "alice", nickname: "" }

Welche wählen? Es hängt ab. null ist explizit – “wir wissen, dass es keinen Nickname gibt”. Fehlendes Field kann bedeuten “wir haben nie gefragt” oder “Schema-Version vor diesem Field”. Empty string ist für Strings eine Option, aber semantisch anders als null. Die Wahl beeinflusst Queries – { nickname: null } matched sowohl null als auch fehlendes Field, was oft nicht gewünscht ist.

35.3 $inc und $mul: Numerische Operationen

Für Counter, Scores, Quantities – numerische Felder, die inkrementiert oder modifiziert werden – gibt es spezielle Operatoren.

$inc – Inkrementieren oder Dekrementieren:

// Login-Counter erhöhen
db.users.updateOne(
  { username: "alice" },
  { $inc: { loginCount: 1 } }
)

// Score um 10 erhöhen
db.players.updateOne(
  { playerId: "P123" },
  { $inc: { score: 10 } }
)

// Quantity reduzieren (negatives Inkrement)
db.inventory.updateOne(
  { productId: "PROD-A" },
  { $inc: { quantity: -5 } }
)

$inc ist atomar – bei konkurrierenden Updates gehen keine Inkremente verloren. Ohne $inc müsste man das Dokument fetchen, den Wert inkrementieren, und zurückschreiben – ein Race-Condition-Alptraum. $inc ist die richtige Lösung für alle Counter-Szenarien.

Wenn das Feld nicht existiert, wird es mit dem Inkrement-Value initialisiert:

// loginCount existiert nicht -> wird auf 1 gesetzt
db.users.updateOne(
  { username: "newuser" },
  { $inc: { loginCount: 1 } }
)

$mul – Multiplizieren:

// Preise um 10% erhöhen (multiply by 1.1)
db.products.updateMany(
  { category: "electronics" },
  { $mul: { price: 1.1 } }
)

$mul multipliziert den bestehenden Wert. Für Prozentsatz-basierte Änderungen ist dies praktischer als manuelles Calculate-and-Set.

35.4 Array-Operatoren: $push, $pull, $addToSet

Arrays sind ein zentraler Teil von MongoDB’s Document-Model. Die Array-Update-Operatoren sind mächtig aber komplex.

$push – Element zu Array hinzufügen:

// Tag zu Array hinzufügen
db.posts.updateOne(
  { postId: "POST-123" },
  { $push: { tags: "mongodb" } }
)

Dies appendet “mongodb” zum Ende des tags-Arrays. Wenn tags nicht existiert, wird es als Array erstellt. Wenn tags kein Array ist (etwa ein String), wirft MongoDB einen Error.

$push mit $each für multiple Elements:

db.posts.updateOne(
  { postId: "POST-123" },
  { $push: { tags: { $each: ["database", "nosql", "tutorial"] } } }
)

Die $each-Syntax pusht multiple Elements auf einmal. Ohne $each würde MongoDB das gesamte Array als einzelnes Element pushen (nested Array).

$push mit Modifiers für kontrollierten Insert:

// Push und sortiere Array, limitiere auf 10 Elemente
db.users.updateOne(
  { username: "alice" },
  { $push: { 
      recentSearches: {
        $each: ["mongodb tutorial"],
        $position: 0,     // Insert at beginning
        $slice: 10        // Keep only first 10
      }
  }}
)

Dies implementiert ein “Recent Searches”-Feature – neue Searches werden an den Anfang gepusht, das Array auf 10 limitiert (älteste werden dropped). Die Modifiers: - $position: Wo im Array inserieren (0 = Anfang) - $slice: Array-Length limitieren (negativ = last N, positiv = first N) - $sort: Array nach dem Push sortieren

$addToSet – Push nur wenn nicht bereits vorhanden:

db.users.updateOne(
  { username: "alice" },
  { $addToSet: { favorites: "PROD-123" } }
)

Dies pusht “PROD-123” zu favorites nur wenn es nicht bereits drin ist. Für Sets (Arrays ohne Duplikate) ist dies die richtige Methode. Man kann $each kombinieren:

db.users.updateOne(
  { username: "alice" },
  { $addToSet: { 
      favorites: { 
        $each: ["PROD-123", "PROD-456", "PROD-789"] 
      }
  }}
)

Jedes Element wird nur hinzugefügt, wenn es noch nicht existiert.

$pull – Element aus Array entfernen:

// Entferne spezifisches Tag
db.posts.updateOne(
  { postId: "POST-123" },
  { $pull: { tags: "deprecated" } }
)

Dies entfernt alle Vorkommen von “deprecated” aus tags. Für Arrays of Objects kann man Query-Syntax nutzen:

// Entferne alle items mit quantity 0
db.orders.updateOne(
  { orderId: "ORD-123" },
  { $pull: { 
      items: { quantity: 0 }
  }}
)

$pop – Erstes oder letztes Element entfernen:

// Letztes Element entfernen
db.users.updateOne(
  { username: "alice" },
  { $pop: { recentSearches: 1 } }
)

// Erstes Element entfernen
db.users.updateOne(
  { username: "alice" },
  { $pop: { recentSearches: -1 } }
)

1 ist last, -1 ist first. Nützlich für Queue- oder Stack-ähnliche Strukturen.

35.5 Positional Operators: Updates auf spezifische Array-Elemente

Für Arrays of Objects ist es oft nötig, ein spezifisches Element zu updaten basierend auf einer Condition. Die Positional-Operatoren $, $[] und $[<identifier>] erlauben dies.

$ – Update das erste matched Element:

// Update das item mit productId "PROD-A"
db.orders.updateOne(
  { 
    orderId: "ORD-123",
    "items.productId": "PROD-A"      // Match condition
  },
  { $set: { "items.$.quantity": 5 } }  // $ referenziert matched element
)

Das $ im Update-Teil referenziert das erste Array-Element, das im Query-Filter matched hat. Dies funktioniert nur, wenn der Filter ein Array-Element matched – sonst ist $ undefined.

$[] – Update alle Array-Elemente:

// Setze shipped: true für alle items
db.orders.updateOne(
  { orderId: "ORD-123" },
  { $set: { "items.$[].shipped": true } }
)

Kein Filter nötig – $[] updated alle Elemente des Arrays.

$[identifier] mit arrayFilters – Conditional Update:

// Update nur items mit quantity > 0
db.orders.updateOne(
  { orderId: "ORD-123" },
  { $set: { "items.$[elem].status": "available" } },
  { arrayFilters: [{ "elem.quantity": { $gt: 0 } }] }
)

Der arrayFilters-Parameter definiert Conditions. elem ist ein Placeholder, der im Update-Teil referenced wird. Dies updated nur Array-Elements, die die Condition erfüllen. Sehr mächtig für komplexe Array-Updates.

Beispiel mit mehreren Filters:

// Update items mit quantity > 0 UND price < 100
db.orders.updateOne(
  { orderId: "ORD-123" },
  { $set: { "items.$[elem].discount": 0.1 } },
  { arrayFilters: [
    { "elem.quantity": { $gt: 0 } },
    { "elem.price": { $lt: 100 } }
  ]}
)

35.6 replaceOne: Komplettes Dokument ersetzen

Statt Feld-Level-Updates kann man das gesamte Dokument ersetzen:

db.users.replaceOne(
  { username: "alice" },
  {
    username: "alice",
    email: "alice@example.com",
    profile: {
      firstName: "Alice",
      lastName: "Smith"
    },
    createdAt: new Date("2024-01-01")
  }
)

Dies überschreibt das komplette Dokument (außer _id, das immutable ist). Alle Felder, die im Replacement nicht sind, werden gelöscht. replaceOne() ist gefährlich – man kann versehentlich Felder verlieren. Update-Operatoren ($set, etc.) sind safer und expliziter.

Use-Case für replaceOne(): Wenn das Dokument fundamental re-structured werden muss, etwa bei Schema-Migrations oder wenn ein externes System ein komplettes neues Dokument liefert.

35.7 Upserts: Insert wenn nicht existiert, Update sonst

Upserts sind eine mächtige MongoDB-Feature – atomic “Insert-or-Update” ohne Race Conditions.

db.counters.updateOne(
  { name: "pageViews" },
  { $inc: { count: 1 } },
  { upsert: true }
)

Beim ersten Call existiert kein Dokument mit name: "pageViews". MongoDB inserted eines mit name: "pageViews" und count: 1. Bei nachfolgenden Calls existiert es bereits, also wird count inkrementiert.

Die Response bei Upsert zeigt die inserted ID:

{
  acknowledged: true,
  matchedCount: 0,
  modifiedCount: 0,
  upsertedId: ObjectId('...')
}

matchedCount: 0 weil das Dokument nicht existierte. upsertedId zeigt die generierte _id des neuen Dokuments.

$setOnInsert – Felder nur beim Insert setzen:

Manchmal will man Felder setzen, die nur bei Insert, nicht bei Update gesetzt werden:

db.users.updateOne(
  { username: "alice" },
  { 
    $set: { lastSeen: new Date() },
    $setOnInsert: { 
      createdAt: new Date(),
      signupSource: "web"
    }
  },
  { upsert: true }
)

Beim Insert werden lastSeen, createdAt und signupSource gesetzt. Bei Update wird nur lastSeen aktualisiert, die anderen bleiben unverändert. Dies ist perfect für “Created/Updated Timestamps”-Patterns.

35.8 findOneAndUpdate: Atomic Read-Modify-Write

Manchmal braucht man das Dokument zurück nach dem Update – etwa um die neue Version zu loggen oder an den Client zu senden. findOneAndUpdate() returned das Dokument:

const updatedUser = db.users.findOneAndUpdate(
  { username: "alice" },
  { $inc: { loginCount: 1 } },
  { returnDocument: "after" }  // Return updated version
)

print(updatedUser.loginCount)  // Der neue Wert

Der returnDocument-Parameter kann “before” (Default) oder “after” sein. “before” returned das Dokument vor dem Update, “after” danach.

Use-Case: Optimistic Locking, Transaction-ähnliche Patterns, oder wenn man den updated Value sofort braucht ohne separates Read.

// Atomic Counter mit Return
const result = db.counters.findOneAndUpdate(
  { name: "orderNumber" },
  { $inc: { sequence: 1 } },
  { returnDocument: "after", upsert: true }
)

const newOrderNumber = result.sequence
// Nutze newOrderNumber für neuen Order

Dies garantiert, dass jeder Order eine unique, sequential Number bekommt ohne Race Conditions.

35.9 Concurrency und Atomicity: Updates in Multi-User-Systemen

MongoDB-Updates sind atomar auf Document-Level. Ein einzelnes Update (auch wenn es multiple Felder ändert) ist eine atomic Operation – entweder alle Änderungen passieren oder keine.

Race Conditions ohne atomare Operatoren:

// SCHLECHT: Read-Modify-Write Race Condition
const user = db.users.findOne({ username: "alice" })
user.loginCount += 1
db.users.updateOne(
  { username: "alice" },
  { $set: { loginCount: user.loginCount } }
)

Wenn zwei Clients diese Sequenz concurrent ausführen: 1. Client A reads loginCount: 10 2. Client B reads loginCount: 10 3. Client A writes loginCount: 11 4. Client B writes loginCount: 11

Resultat: Zwei Logins, aber loginCount ist 11, nicht 12. Ein Login ging verloren.

Richtig mit $inc:

db.users.updateOne(
  { username: "alice" },
  { $inc: { loginCount: 1 } }
)

Dies ist atomar. Concurrent Updates werden serialized vom Server – beide Inkremente werden korrekt angewendet.

Optimistic Locking Pattern:

Für komplexere Updates, wo man Read-Modify-Write braucht, kann man eine Version-Number nutzen:

// Read mit Version
const user = db.users.findOne({ username: "alice" })
const currentVersion = user.version

// Modify
user.email = "newemail@example.com"
user.version += 1

// Write nur wenn Version unverändert
const result = db.users.updateOne(
  { username: "alice", version: currentVersion },
  { $set: { 
      email: user.email,
      version: user.version
  }}
)

if (result.matchedCount === 0) {
  print("Conflict: Document was modified by another client")
  // Retry or handle conflict
}

Wenn ein anderer Client das Dokument zwischenzeitlich updated hat, ist die Version anders und das Update matcht nicht. Dies detektiert Conflicts und erlaubt Retry-Logik.

35.10 Performance Considerations: Index-Nutzung bei Updates

Updates nutzen Indexes für das Finden der zu updatendenden Dokumente, genauso wie Queries. Ein Update ohne passenden Index scannt die gesamte Collection:

// Langsam ohne Index auf email
db.users.updateOne(
  { email: "alice@example.com" },
  { $set: { lastSeen: new Date() } }
)
// -> COLLSCAN

// Mit Index
db.users.createIndex({ email: 1 })
// Jetzt: IXSCAN

Aber Updates haben einen zusätzlichen Performance-Aspekt: Index-Maintenance. Jedes Update, das ein indiziertes Feld ändert, muss den Index updaten:

// Drei Indexes: email, status, createdAt
db.users.createIndex({ email: 1 })
db.users.createIndex({ status: 1 })
db.users.createIndex({ createdAt: 1 })

// Update ändert email -> muss email-Index updaten
db.users.updateOne(
  { username: "alice" },
  { $set: { email: "newemail@example.com" } }
)

Viele Indexes machen Writes langsamer. Der Trade-off: Indexes beschleunigen Reads dramatisch, aber verlangsamen Writes. Für Read-Heavy-Workloads ist dies akzeptabel. Für Write-Heavy-Workloads sollte man Indexes minimieren.

35.11 Bulk-Updates: Effizient viele Dokumente updaten

Für Bulk-Operations ist bulkWrite() effizienter als Loop mit einzelnen Updates:

const operations = [
  {
    updateOne: {
      filter: { username: "alice" },
      update: { $set: { status: "active" } }
    }
  },
  {
    updateOne: {
      filter: { username: "bob" },
      update: { $inc: { loginCount: 1 } }
    }
  },
  {
    updateMany: {
      filter: { status: "inactive", lastSeen: { $lt: oldDate } },
      update: { $set: { archived: true } }
    }
  }
]

db.users.bulkWrite(operations)

Alle Operations werden in einem Batch executed – ein Netzwerk-Roundtrip statt dutzende. Performance-Gewinn ist signifikant bei großen Bulk-Updates.

Die folgende Tabelle fasst Update-Operatoren zusammen:

Operator Zweck Atomicity Use-Case
$set Feld setzen/überschreiben Atomic Jegliche Field-Updates
$unset Feld entfernen Atomic Schema-Cleanup, Privacy
$inc Numerisch inkrementieren Atomic Counters, Scores
$mul Numerisch multiplizieren Atomic Prozentsatz-Änderungen
$push Element zu Array adden Atomic Tags, Logs, Listen
$pull Element aus Array entfernen Atomic Cleanup, Removal
$addToSet Add wenn nicht existent Atomic Sets, Unique-Listen
$ Update matched Array-Element Atomic Conditional Array-Update
$[] Update alle Array-Elemente Atomic Bulk-Array-Modify
$[id] Update filtered Array-Elemente Atomic Complex Array-Updates

Update-Operationen sind mächtiger und komplexer als simples Überschreiben. Die granularen Operatoren erlauben effiziente, atomare Modifications ohne Read-Modify-Write-Cycles. Aber sie erfordern Verständnis – der falsche Operator kann zu subtilen Bugs führen. $set statt $inc für Counter verliert Updates bei Concurrency. $push ohne $each pusht nested Arrays statt Elements. replaceOne() löscht versehentlich Felder. Die Best Practice: Nutze den spezifischsten Operator für den Use-Case, teste Concurrency-Scenarios, und überwache Performance via Slow-Query-Logs. Mit korrektem Operator-Einsatz sind Updates nicht nur funktional korrekt, sondern auch performant und robust gegen Race Conditions.