33 CRUD: Create – Die Kunst des Einfügens

CRUD – Create, Read, Update, Delete – sind die fundamentalen Operationen jeder Datenbank. Sie sind das Alphabet, aus dem alle Datenbank-Interaktionen gebaut sind. Bei MongoDB sind diese Operationen elegant und intuitiv, basierend auf JSON-ähnlicher Syntax. Aber hinter der Einfachheit stecken Nuancen – Performance-Implikationen, Atomicity-Garantien, Error-Handling-Strategien und Best Practices, die den Unterschied zwischen fragilen Prototypen und robusten Produktions-Systemen ausmachen.

Dieses Kapitel fokussiert auf Create – das Einfügen von Daten. Die anderen CRUD-Operationen werden in separaten Kapiteln behandelt. Create scheint simpel: Dokument erstellen, insertOne() callen, fertig. Aber produktive Systeme haben Requirements, die über Hello-World hinausgehen: Bulk-Inserts mit tausenden Dokumenten, Error-Handling bei Constraint-Violations, Upserts für idempotente Operationen, Write Concerns für Durability-Garantien. Diese Komplexität zu verstehen ist essentiell für robuste Anwendungen.

33.1 insertOne: Der einfachste Fall

Die insertOne()-Methode fügt ein einzelnes Dokument in eine Collection ein. Die Syntax ist minimal:

const result = db.users.insertOne({
  username: "alice",
  email: "alice@example.com",
  createdAt: new Date()
})

printjson(result)

Die Response zeigt Success-Status und die generierte _id:

{
  acknowledged: true,
  insertedId: ObjectId('65abc123def456789012345')
}

Das acknowledged: true bedeutet, dass MongoDB den Insert bestätigt hat. Das insertedId ist die _id des neuen Dokuments. Wenn man keine _id explizit angibt, generiert MongoDB automatisch eine ObjectId.

**Explizite _id setzen:**

Man kann eigene _id-Werte verwenden, aber sie müssen unique innerhalb der Collection sein:

db.users.insertOne({
  _id: "user-12345",
  username: "bob",
  email: "bob@example.com"
})

String-IDs sind valide, aber man verliert die Vorteile von ObjectIds – embedded Timestamp, automatische Uniqueness ohne Koordination. Für die meisten Anwendungen sind ObjectIds die bessere Wahl.

Duplicate Key Errors:

Wenn man versucht, ein Dokument mit existierendem _id einzufügen, wirft MongoDB einen Duplicate-Key-Error:

db.users.insertOne({ _id: ObjectId('65abc123def456789012345'), username: "charlie" })
// MongoServerError: E11000 duplicate key error

Dieser Error ist nicht catchable durch Schema-Validation – er ist ein fundamentaler Constraint. Applications müssen dies handhaben, typischerweise mit try-catch:

try {
  db.users.insertOne({ 
    username: "alice",
    email: "alice@example.com"
  })
  print("Insert successful")
} catch (error) {
  if (error.code === 11000) {
    print("Duplicate key - user already exists")
  } else {
    print("Unknown error:", error.message)
  }
}

Der Error-Code 11000 ist spezifisch für Duplicate-Key. Applications können darauf reagieren – etwa indem sie einen alternativen Username vorschlagen oder ein Update statt Insert durchführen.

33.2 insertMany: Bulk-Inserts für Performance

Das Einfügen von tausenden Dokumenten mit einzelnen insertOne()-Calls ist ineffizient – jeder Call ist ein Netzwerk-Roundtrip. insertMany() batched multiple Inserts in eine Operation:

const users = [
  { username: "user1", email: "user1@example.com" },
  { username: "user2", email: "user2@example.com" },
  { username: "user3", email: "user3@example.com" }
]

const result = db.users.insertMany(users)
printjson(result)

Response:

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('...'),
    '1': ObjectId('...'),
    '2': ObjectId('...')
  }
}

Das insertedIds-Objekt mapped Array-Indizes zu generierten ObjectIds. Alle drei Dokumente wurden in einer Operation inserted – ein Roundtrip statt drei.

Performance-Gewinn:

Der Performance-Unterschied ist signifikant. Ein Benchmark: 10.000 Dokumente einfügen.

Die Batch-Operation ist 15x schneller. Für Bulk-Data-Loads – etwa initiale Datenbank-Population oder Batch-ETL-Jobs – ist insertMany() essentiell.

Ordered vs. Unordered Inserts:

Per Default sind insertMany()-Inserts “ordered” – sie werden sequenziell verarbeitet. Schlägt eines fehl, stoppen nachfolgende:

db.users.insertMany([
  { _id: 1, username: "user1" },
  { _id: 2, username: "user2" },
  { _id: 1, username: "user3" },  // Duplicate _id
  { _id: 3, username: "user4" }
])
// Error: Duplicate key
// Nur user1 und user2 sind inserted, user4 nicht

Mit ordered: false versucht MongoDB, alle Dokumente einzufügen, selbst wenn manche fehlschlagen:

db.users.insertMany([
  { _id: 1, username: "user1" },
  { _id: 2, username: "user2" },
  { _id: 1, username: "user3" },  // Duplicate
  { _id: 3, username: "user4" }
], { ordered: false })
// user1, user2 und user4 sind inserted
// user3 failed, aber der Rest continued

Die Response zeigt welche Inserts failed:

{
  acknowledged: true,
  insertedIds: {
    '0': 1,
    '1': 2,
    '3': 3
  },
  writeErrors: [{
    index: 2,
    code: 11000,
    errmsg: "E11000 duplicate key error..."
  }]
}

Unordered-Inserts sind nützlich für Best-Effort-Bulk-Loads, wo man maximale Durchsatz will und partielle Failures akzeptabel sind.

33.3 Write Concerns: Durability vs. Performance

Inserts (und alle Writes) können mit Write Concerns konfiguriert werden, die bestimmen, wann MongoDB einen Write als erfolgreich betrachtet. Der Default Write Concern ist { w: 1 } – acknowledge, sobald der Primary den Write akzeptiert hat.

db.users.insertOne(
  { username: "alice", email: "alice@example.com" },
  { writeConcern: { w: 1 } }
)

Dies ist schnell, aber nicht ultra-safe. Der Primary könnte crashen, bevor die Daten zu Secondaries repliziert wurden. Für kritische Daten will man w: "majority":

db.users.insertOne(
  { username: "alice", email: "alice@example.com" },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
)

Dies wartet, bis eine Mehrheit des Replica Sets den Write bestätigt hat. Bei einem 3-Node-Replica Set heißt das: Mindestens 2 Nodes müssen den Write haben. Dies ist langsamer (mehr Latenz), aber sicherer – selbst wenn der Primary crasht, sind die Daten auf mindestens einem Secondary.

Der wtimeout-Parameter ist ein Safety-Net: Wenn die Majority-Acknowledgement nicht innerhalb 5 Sekunden erfolgt, wirft MongoDB einen Error. Ohne Timeout könnte der Write indefinitely hängen bei Netzwerk-Problemen oder Replica-Set-Failures.

Trade-off-Tabelle:

Write Concern Latenz Durability Use-Case
w: 1 Niedrig Moderat Unkritische Daten, Performance wichtig
w: "majority" Höher Hoch Kritische Daten, Consistency wichtig
w: 0 Minimal Sehr niedrig Logging, Metrics (Datenverlust akzeptabel)
w: 3 (explizit) Höchste Höchste Mission-critical (bei 5+ Node Cluster)

Für die meisten Anwendungen ist der Default w: 1 ausreichend. Für Finanztransaktionen, User-Account-Creation oder andere kritische Operations sollte man w: "majority" nutzen.

33.4 Schema Validation: Constraints beim Insert

MongoDB ist schemaless, aber Schema-Validation kann beim Insert erzwingen, dass Dokumente bestimmte Strukturen haben. Validation wird beim Collection-Create oder via collMod definiert:

db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["username", "email"],
      properties: {
        username: {
          bsonType: "string",
          minLength: 3,
          maxLength: 30,
          pattern: "^[a-zA-Z0-9_]+$"
        },
        email: {
          bsonType: "string",
          pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
        },
        age: {
          bsonType: "int",
          minimum: 0,
          maximum: 150
        }
      }
    }
  },
  validationLevel: "strict",
  validationAction: "error"
})

Jetzt werden Inserts, die die Validation nicht erfüllen, rejected:

db.users.insertOne({ username: "ab", email: "invalid" })
// Error: Document failed validation
// - username too short (< 3 chars)
// - email invalid format

Die Validation ist sehr mächtig – sie kann komplexe Constraints ausdrücken: Required-Fields, Type-Checks, Regex-Patterns, Numeric-Ranges, Array-Length-Constraints, nested Object-Validations. Für Produktions-Systeme, wo Data-Quality kritisch ist, ist Schema-Validation nicht optional.

Die validationLevel kann “strict” (alle Dokumente) oder “moderate” (nur neue/modifizierte Dokumente) sein. Die validationAction kann “error” (reject) oder “warn” (log aber allow) sein. Für Production ist “strict” und “error” empfohlen.

33.5 Upserts: Insert oder Update, idempotent

Manchmal weiß man nicht, ob ein Dokument existiert oder nicht. Man will: “Insert wenn neu, update wenn existiert”. Dies ist ein Upsert, implementiert mit updateOne() oder replaceOne() mit der upsert: true-Option:

db.counters.updateOne(
  { name: "pageViews" },        // Filter
  { $inc: { count: 1 } },       // Update
  { upsert: true }               // Insert wenn nicht existiert
)

Beim ersten Call existiert kein Dokument mit name: "pageViews". MongoDB inserted eines mit name: "pageViews" und count: 1. Bei nachfolgenden Calls wird das existierende Dokument ge-updated, count wird inkrementiert.

Upserts sind atomic – keine Race Conditions zwischen “Check if exists” und “Insert or Update”. Zwei concurrent Upserts führen zu einem Insert und einem Update, nie zu zwei Inserts.

Mit setOnInsert:

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

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

Beim ersten Call (Insert) werden lastSeen, createdAt und signupSource gesetzt. Bei nachfolgenden Calls (Update) wird nur lastSeen aktualisiert, createdAt bleibt unverändert.

33.6 Bulk Write API: Flexibles Batch-Processing

Für komplexe Bulk-Operations, die Mix aus Inserts, Updates und Deletes sind, gibt es die Bulk Write API:

db.users.bulkWrite([
  {
    insertOne: {
      document: { username: "user1", email: "user1@example.com" }
    }
  },
  {
    updateOne: {
      filter: { username: "user2" },
      update: { $set: { email: "newemail@example.com" } }
    }
  },
  {
    deleteOne: {
      filter: { username: "user3" }
    }
  },
  {
    replaceOne: {
      filter: { username: "user4" },
      replacement: { username: "user4", email: "user4@example.com", status: "active" }
    }
  }
])

Alle Operations werden in einem Batch executed. Dies ist effizienter als separate Calls und erlaubt atomare Multi-Operation-Workflows (allerdings ohne vollständige Transaction-Semantik – jede Operation ist atomar, aber das gesamte Batch nicht).

Die bulkWrite()-API unterstützt auch ordered und writeConcern-Options wie insertMany().

33.7 Embedded Documents und Arrays: Strukturierte Inserts

MongoDB’s Document-Model erlaubt beliebig verschachtelte Strukturen. Beim Insert können Dokumente embedded Sub-Documents und Arrays enthalten:

db.orders.insertOne({
  orderId: "ORD-2024-001",
  customerId: "CUST-123",
  orderDate: new Date(),
  items: [
    {
      productId: "PROD-A",
      quantity: 2,
      price: 49.99
    },
    {
      productId: "PROD-B",
      quantity: 1,
      price: 149.99
    }
  ],
  shippingAddress: {
    street: "123 Main St",
    city: "Berlin",
    postalCode: "10115",
    country: "DE"
  },
  total: 249.97
})

Dieses Dokument hat drei Ebenen Verschachtelung: Root-Level-Felder, ein items-Array von Objects, und ein shippingAddress-Embedded-Document. MongoDB speichert dies effizient als BSON. Queries können auf nested Felder zugreifen mit Dot-Notation:

db.orders.find({ "shippingAddress.city": "Berlin" })
db.orders.find({ "items.productId": "PROD-A" })

Die Verschachtelung erlaubt reichere Datenmodelle als flache relationale Schemas, aber sie erfordert sorgfältiges Design – zu tiefe Verschachtelung macht Queries komplex und Updates schwierig.

33.8 Best Practices für Inserts

1. Batch-Inserts nutzen: Für mehr als 10-20 Dokumente immer insertMany() oder bulkWrite() statt Loop mit insertOne().

**2. Explizite _id nur wenn nötig:** ObjectIds sind designed für MongoDB. Custom-IDs nur wenn es gute Gründe gibt (externe System-Integration, natürliche Keys).

3. Schema-Validation für kritische Collections: Data-Quality-Problems sind schwer zu fixen nach dem Fact. Validation verhindert sie proaktiv.

4. Write Concerns für kritische Daten: w: "majority" für Account-Creation, Financial-Transactions, jede Operation wo Datenverlust inakzeptabel ist.

5. Error-Handling immer implementieren: Inserts können fehlschlagen – Duplicate Keys, Validation-Errors, Network-Timeouts. Applications müssen diese Errors gracefully handhaben.

6. Idempotente Inserts via Upserts: Für Systeme, wo Operations re-tried werden können, machen Upserts die Operation idempotent – mehrmaliges Ausführen ist safe.

7. Indexes vor Bulk-Inserts: Große Bulk-Inserts in Collections mit vielen Indexes sind langsam (jeder Insert muss alle Indexes updaten). Für massive Bulk-Loads kann man Indexes temporär droppen, Daten laden, Indexes rebuilden.

Die folgende Tabelle fasst Insert-Methoden zusammen:

Methode Use-Case Performance Atomicity
insertOne() Einzelnes Dokument 1 Roundtrip Atomic
insertMany() Bulk-Inserts (homogene Dokumente) 1 Roundtrip für alle Ordered: Sequential
Unordered: Best-effort
bulkWrite() Gemischte Operations (Insert/Update/Delete) 1 Roundtrip für Batch Jede Op atomic, Batch nicht
updateOne(..., {upsert: true}) Idempotente Insert-or-Update 1 Roundtrip Atomic

Create-Operationen sind der Einstiegspunkt für Daten in MongoDB. Sie scheinen simpel, aber robuste Applications müssen Nuancen verstehen – Write Concerns für Durability, Schema-Validation für Data-Quality, Bulk-APIs für Performance, Error-Handling für Resilienz. Die Defaults funktionieren für Prototypen, aber Production erfordert bewusste Entscheidungen über diese Trade-offs. Mit dem richtigen Verständnis wird Create von trivialem API-Call zu fundierter Architektur-Entscheidung.