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.
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 errorDieser 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.
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.
insertOne() in Loop: ~30 Sekunden (10.000
Roundtrips)insertMany() mit allen 10.000: ~2 Sekunden (1
Roundtrip)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 nichtMit 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 continuedDie 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.
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.
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 formatDie 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.
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.
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().
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.
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.