mongosh ist nicht nur ein interaktives Tool, sondern eine vollwertige JavaScript-Runtime für MongoDB-Automatisierung. Was in der Shell interaktiv funktioniert, kann in Scripts verpackt und automatisiert werden – Daten-Migrationen, Backup-Verifications, Cleanup-Jobs, Report-Generierung, Schema-Validierungen. Für Teams, die MongoDB in Produktion betreiben, ist Scripting nicht optional, sondern essentiell.
Der Unterschied zwischen “schnell etwas in der Shell ausprobieren” und “robustes Production-Script schreiben” ist erheblich. Interactive Commands können fehlschlagen und man sieht den Error sofort. Scripts laufen unbeaufsichtigt, oft in CI/CD-Pipelines oder Cron-Jobs. Sie müssen robust sein – Fehler handhaben, loggen, sauber aufräumen und mit aussagekräftigen Exit-Codes terminieren. Dieses Kapitel behandelt, wie man von simplen Scripts zu production-ready Automation kommt.
Der Übergang von interaktiver Shell zu Script ist trivial. Was in
mongosh interaktiv funktioniert, funktioniert in einer
.js-Datei:
// insert-users.js
db.users.insertMany([
{ username: "alice", email: "alice@example.com", createdAt: new Date() },
{ username: "bob", email: "bob@example.com", createdAt: new Date() },
{ username: "charlie", email: "charlie@example.com", createdAt: new Date() }
])
print("Inserted 3 users")Ausführung:
mongosh localhost:27017/myapp --quiet --file insert-users.jsDas --quiet-Flag unterdrückt den mongosh-Banner und
Startup-Messages, was Output cleaner macht für Scripting. Das
--file-Flag lädt und executed die Datei.
Die Alternative zu --file ist Stdin-Redirection:
mongosh localhost:27017/myapp < insert-users.jsBeide Methoden funktionieren identisch. Die
--file-Syntax ist expliziter und moderner.
Einfache Scripts sind linear – eine Sequenz von Statements. Aber komplexere Tasks profitieren von Modularisierung: Funktionen, die einzelne Aufgaben kapseln, wiederverwendbar sind und testbar.
// migration.js
// Funktion für einzelne Migration
function addCountryFieldToUsers() {
const result = db.users.updateMany(
{ country: { $exists: false } },
{ $set: { country: "UNKNOWN" } }
)
print(`Added country field to ${result.modifiedCount} users`)
return result.modifiedCount
}
// Funktion für Validation
function validateAllUsersHaveCountry() {
const missingCount = db.users.countDocuments({
country: { $exists: false }
})
if (missingCount > 0) {
throw new Error(`${missingCount} users still missing country field`)
}
print("Validation passed: All users have country field")
}
// Haupt-Workflow
print("Starting migration...")
const modifiedCount = addCountryFieldToUsers()
print("Validating migration...")
validateAllUsersHaveCountry()
print("Migration completed successfully")Diese Struktur ist testbar – jede Funktion kann isoliert gecalled werden. Sie ist auch lesbar – der Haupt-Workflow ist klar, die Details sind in Funktionen gekapselt.
Scripts, die in Production laufen, müssen Fehler graceful handhaben.
Ein Crash mitten in einer Migration kann inkonsistente Daten
hinterlassen. JavaScript’s try-catch ist essentiell:
// robust-insert.js
function insertUser(userData) {
try {
const result = db.users.insertOne(userData)
print(`Inserted user: ${userData.username}`)
return { success: true, id: result.insertedId }
} catch (error) {
print(`ERROR inserting user ${userData.username}: ${error.message}`)
return { success: false, error: error.message }
}
}
const usersToInsert = [
{ username: "alice", email: "alice@example.com" },
{ username: "bob", email: "bob@example.com" },
{ username: "alice", email: "alice2@example.com" } // Duplicate username
]
const results = usersToInsert.map(insertUser)
// Summary
const successCount = results.filter(r => r.success).length
const failureCount = results.filter(r => !r.success).length
print(`\nSummary: ${successCount} succeeded, ${failureCount} failed`)
if (failureCount > 0) {
print("Some inserts failed. Check errors above.")
quit(1) // Exit mit Error-Code
}Dieses Script versucht, alle Users einzufügen, selbst wenn manche fehlschlagen (etwa wegen Duplicate-Key-Errors). Es sammelt Results, gibt ein Summary und exitiert mit Error-Code wenn Failures auftraten. Dies ist robuster als “fail on first error” – man sieht alle Probleme auf einmal, nicht eins nach dem anderen.
In CI/CD-Pipelines oder Cron-Jobs muss ein Script seinen
Success-Status kommunizieren. Unix-Convention: Exit-Code 0 bedeutet
Success, Non-Zero bedeutet Failure. mongosh unterstützt dies mit
quit():
// check-health.js
const userCount = db.users.countDocuments()
const minExpectedUsers = 100
if (userCount < minExpectedUsers) {
print(`ERROR: Only ${userCount} users, expected at least ${minExpectedUsers}`)
quit(1)
}
print(`Health check passed: ${userCount} users found`)
quit(0)Shell-Scripts können den Exit-Code prüfen:
#!/bin/bash
mongosh localhost:27017/myapp --quiet --file check-health.js
if [ $? -ne 0 ]; then
echo "Health check failed! Alerting team..."
# Send alert
exit 1
fi
echo "Health check passed"Der $? in Bash ist der Exit-Code des letzten Commands.
Non-Zero triggert das Alert-System.
Hardcoded-Values in Scripts sind inflexibel. Was wenn man denselben Migration-Script in Dev, Staging und Production laufen will, aber mit unterschiedlichen Parametern?
mongosh-Scripts können auf Environment-Variables zugreifen:
// configurable-cleanup.js
const daysThreshold = parseInt(process.env.DAYS_THRESHOLD || "90")
const dryRun = process.env.DRY_RUN === "true"
print(`Cleanup configuration:`)
print(` Days threshold: ${daysThreshold}`)
print(` Dry run: ${dryRun}`)
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - daysThreshold)
print(`\nFinding old records (before ${cutoffDate.toISOString()})...`)
const oldRecords = db.logs.find({
createdAt: { $lt: cutoffDate }
}).toArray()
print(`Found ${oldRecords.length} old records`)
if (dryRun) {
print("DRY RUN: Would delete these records (but not actually deleting)")
quit(0)
}
print("Deleting old records...")
const result = db.logs.deleteMany({
createdAt: { $lt: cutoffDate }
})
print(`Deleted ${result.deletedCount} records`)Nutzung:
# Dry run mit 30 Tagen
DAYS_THRESHOLD=30 DRY_RUN=true mongosh --quiet --file configurable-cleanup.js
# Production run mit 90 Tagen
DAYS_THRESHOLD=90 DRY_RUN=false mongosh --quiet --file configurable-cleanup.jsEnvironment-Variables sind der idiomatische Weg für Script-Konfiguration in Unix-Umgebungen. Sie integrieren sauber mit CI/CD-Systemen, die oft Environment-Variables für Configuration nutzen.
Größere Projekte sollten Code nicht duplizieren. Die
load()-Funktion erlaubt, JavaScript-Files zu
importieren:
// utils.js
function logWithTimestamp(message) {
const timestamp = new Date().toISOString()
print(`[${timestamp}] ${message}`)
}
function countDocuments(collectionName) {
return db.getCollection(collectionName).countDocuments()
}// main-script.js
load("./utils.js")
logWithTimestamp("Starting migration...")
const userCount = countDocuments("users")
logWithTimestamp(`Found ${userCount} users`)
// ... rest of scriptDie load()-Function executed die File im Current-Scope.
Alle Funktionen und Variablen aus utils.js sind in
main-script.js verfügbar. Dies ist kein ES6-Module-System
(kein import/export), sondern einfache
Script-Inclusion, aber es funktioniert für Modularisierung.
Der relative Path zu load() ist relativ zum
Current-Working-Directory, nicht zur Script-File. Für robustere
Path-Handling kann man __dirname-ähnliche Patterns
emulieren:
// Nicht direkt in mongosh verfügbar, aber pattern:
const scriptDir = "./scripts"
load(scriptDir + "/utils.js")Ein häufiger Use-Case: Iteriere über alle Dokumente einer Collection, transformiere sie, update sie. Die naive Implementierung ist ineffizient:
// Langsam: Einzelne Updates
db.users.find().forEach(user => {
db.users.updateOne(
{ _id: user._id },
{ $set: { updatedAt: new Date() } }
)
})Für jedes Dokument: Ein Find, ein Update – tausende Roundtrips bei großen Collections. Besser: Batch-Updates mit Aggregation oder Bulk-Operations:
// Schneller: Single Bulk Update
db.users.updateMany(
{}, // Alle Dokumente
{ $set: { updatedAt: new Date() } }
)Wenn die Transformation komplex ist und nicht als Update-Operator ausdrückbar, nutze Bulk-Write-API:
// Bulk-Write für komplexe Transformationen
const operations = []
db.users.find().forEach(user => {
const newEmail = user.email.toLowerCase() // Transformation
operations.push({
updateOne: {
filter: { _id: user._id },
update: { $set: { email: newEmail } }
}
})
// Flush alle 1000 Operations
if (operations.length >= 1000) {
db.users.bulkWrite(operations)
print(`Processed ${operations.length} documents`)
operations.length = 0 // Clear array
}
})
// Flush remaining
if (operations.length > 0) {
db.users.bulkWrite(operations)
print(`Processed final ${operations.length} documents`)
}Diese Batching-Strategy reduziert Roundtrips dramatisch. Statt 1 Million einzelner Updates gibt es 1000 Batch-Updates mit je 1000 Operations. Die Performance-Differenz ist Größenordnungen.
Für Operations, die mehrere Collections oder Dokumente atomar ändern müssen, nutzt man Transactions:
// transfer-balance.js
const session = db.getMongo().startSession()
try {
session.startTransaction()
const fromAccount = session.getDatabase("bank").accounts.findOne(
{ accountId: "A123" },
{ session }
)
const toAccount = session.getDatabase("bank").accounts.findOne(
{ accountId: "B456" },
{ session }
)
const amount = 100
if (fromAccount.balance < amount) {
throw new Error("Insufficient balance")
}
session.getDatabase("bank").accounts.updateOne(
{ accountId: "A123" },
{ $inc: { balance: -amount } },
{ session }
)
session.getDatabase("bank").accounts.updateOne(
{ accountId: "B456" },
{ $inc: { balance: amount } },
{ session }
)
session.commitTransaction()
print(`Transferred ${amount} from A123 to B456`)
} catch (error) {
print(`ERROR: ${error.message}`)
session.abortTransaction()
quit(1)
} finally {
session.endSession()
}Transactions garantieren, dass entweder beide Updates passieren oder keine. Im Fehlerfall (Insufficient Balance, Netzwerk-Fehler) werden alle Änderungen rollbacked.
Wichtig: Die session-Parameter überall –
findOne(), updateOne() etc. Ohne Session sind
die Operations nicht Teil der Transaction.
Production-Scripts sollten ausführlich loggen, was sie tun. Dies hilft beim Debugging wenn etwas schiefgeht. MongoDB hat keine native Logging-Library in Scripts, aber man kann einfache Patterns nutzen:
// logger.js
const LOG_LEVEL = process.env.LOG_LEVEL || "INFO"
const levels = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3
}
function log(level, message) {
if (levels[level] >= levels[LOG_LEVEL]) {
const timestamp = new Date().toISOString()
print(`[${timestamp}] [${level}] ${message}`)
}
}
function debug(msg) { log("DEBUG", msg) }
function info(msg) { log("INFO", msg) }
function warn(msg) { log("WARN", msg) }
function error(msg) { log("ERROR", msg) }Nutzung:
load("./logger.js")
info("Starting data migration")
debug(`Connection to ${db.getMongo().host}`)
const count = db.users.countDocuments()
info(`Found ${count} users to migrate`)
// ... migration logic
info("Migration completed successfully")Mit LOG_LEVEL=DEBUG sieht man detaillierte Logs, mit
LOG_LEVEL=ERROR nur Errors. Dies ist nützlich in Production
(weniger Noise) vs. Debugging (mehr Details).
Ein realistisches Migrations-Script kombiniert all diese Patterns:
// migrate-user-schema-v2.js
load("./logger.js")
load("./utils.js")
const DRY_RUN = process.env.DRY_RUN === "true"
const BATCH_SIZE = parseInt(process.env.BATCH_SIZE || "1000")
info("=== User Schema Migration v2 ===")
info(`Configuration: DRY_RUN=${DRY_RUN}, BATCH_SIZE=${BATCH_SIZE}`)
// Validation
function validatePreconditions() {
info("Validating preconditions...")
const usersCount = db.users.countDocuments()
if (usersCount === 0) {
throw new Error("No users found - wrong database?")
}
info(`Found ${usersCount} users`)
}
// Migration Logic
function migrateUsers() {
info("Starting user migration...")
let processedCount = 0
let migratedCount = 0
const operations = []
db.users.find({
schemaVersion: { $ne: 2 }
}).forEach(user => {
processedCount++
// Transform: Add fullName field
const fullName = `${user.firstName || ""} ${user.lastName || ""}`.trim()
// Transform: Normalize email
const email = user.email ? user.email.toLowerCase() : null
operations.push({
updateOne: {
filter: { _id: user._id },
update: {
$set: {
fullName: fullName,
email: email,
schemaVersion: 2,
migratedAt: new Date()
}
}
}
})
// Batch execute
if (operations.length >= BATCH_SIZE) {
if (!DRY_RUN) {
const result = db.users.bulkWrite(operations)
migratedCount += result.modifiedCount
}
info(`Processed ${processedCount} users (${migratedCount} migrated)`)
operations.length = 0
}
})
// Flush remaining
if (operations.length > 0) {
if (!DRY_RUN) {
const result = db.users.bulkWrite(operations)
migratedCount += result.modifiedCount
}
}
info(`Total: Processed ${processedCount}, Migrated ${migratedCount}`)
return { processedCount, migratedCount }
}
// Post-Migration Validation
function validateMigration() {
info("Validating migration results...")
const unmigrated = db.users.countDocuments({
schemaVersion: { $ne: 2 }
})
if (unmigrated > 0) {
throw new Error(`${unmigrated} users still on old schema!`)
}
info("Validation passed: All users on schema v2")
}
// Main Execution
try {
validatePreconditions()
const results = migrateUsers()
if (DRY_RUN) {
info("DRY RUN: No changes made to database")
info(`Would have migrated ${results.processedCount} users`)
} else {
validateMigration()
info("Migration completed successfully!")
}
quit(0)
} catch (error) {
error(`FATAL: ${error.message}`)
error(error.stack)
quit(1)
}Dieses Script: - Nutzt Environment-Variables für Configuration - Logged ausführlich mit Timestamps - Batched Updates für Performance - Unterstützt Dry-Run-Mode - Validiert Preconditions und Post-Conditions - Handelt Errors gracefully mit Exit-Codes
Für Production-Deployments würde man es in einem Wrapper-Script callen:
#!/bin/bash
# run-migration.sh
set -e # Exit on error
echo "=== Starting Migration ==="
# Dry run first
echo "Running dry run..."
DRY_RUN=true LOG_LEVEL=INFO mongosh "$MONGODB_URI" \
--quiet --file migrate-user-schema-v2.js
echo ""
read -p "Dry run completed. Proceed with actual migration? (yes/no) " confirm
if [ "$confirm" != "yes" ]; then
echo "Migration aborted by user"
exit 0
fi
# Actual migration
echo "Running actual migration..."
DRY_RUN=false LOG_LEVEL=INFO mongosh "$MONGODB_URI" \
--quiet --file migrate-user-schema-v2.js
echo ""
echo "=== Migration Completed ==="Dieses Wrapper-Script führt erst einen Dry-Run aus, wartet auf User-Confirmation und führt dann die eigentliche Migration aus. Für automatisierte Environments würde man die Confirmation skippen.
Große Collections erfordern Performance-Awareness in Scripts:
Index Nutzen: Queries in Scripts sollten indiziert sein. Ein Script, das Millionen Dokumente scannt, ist inakzeptabel langsam:
// Langsam: Full Collection Scan
db.users.find({ email: "alice@example.com" }).forEach(...)
// Schnell: Nutzt Index auf email
db.users.createIndex({ email: 1 }) // Falls noch nicht existiert
db.users.find({ email: "alice@example.com" }).forEach(...)Projection nutzen: Wenn man nur wenige Felder braucht, nicht alle laden:
// Lädt gesamte Dokumente (potentiell MB pro Dokument)
db.users.find().forEach(user => { /* ... */ })
// Lädt nur benötigte Felder
db.users.find({}, { username: 1, email: 1 }).forEach(user => { /* ... */ })Cursor-Timeouts handhaben: Lange-laufende Scripts können Cursor-Timeouts erleben. Der Default-Timeout ist 10 Minuten. Für sehr lange Operationen:
// Disable Cursor Timeout
db.users.find().noCursorTimeout().forEach(user => {
// Lange-laufende Verarbeitung
})Aber: noCursorTimeout() bedeutet, der Cursor bleibt
offen bis manuell geschlossen. Bei Crashes oder Script-Fehlern können
Cursors leaken. Besser: Design Scripts so, dass sie innerhalb Timeout
fertig sind, oder verwende Resumable-Patterns.
MongoDB-Scripts integrieren natürlich in CI/CD-Pipelines. Ein typisches Szenario: Automatische Migrations beim Deployment.
# .gitlab-ci.yml
migrate_db:
stage: deploy
script:
- echo "Running database migration..."
- mongosh "$MONGODB_URI" --quiet --file migrations/v2-add-country-field.js
- echo "Migration completed"
only:
- main
environment:
name: productionDie Pipeline führt das Migrations-Script bei jedem Deployment aus. Der Exit-Code des Scripts bestimmt, ob die Pipeline succeeds oder fails.
Die folgende Tabelle fasst Best Practices zusammen:
| Aspekt | Bad Practice | Good Practice |
|---|---|---|
| Error Handling | Keine try-catch | Umfassende try-catch mit Logging |
| Exit Codes | Immer exit 0 | exit 0 bei Success, 1+ bei Failure |
| Configuration | Hardcoded Values | Environment Variables |
| Logging | Kein oder minimales Logging | Strukturiertes Logging mit Levels |
| Performance | Einzelne Updates in Loop | Batch-Operations |
| Testing | Direkt in Production | Dry-Run-Mode, Test in Staging |
| Validation | Keine Post-Checks | Pre- und Post-Validierung |
| Modularität | Monolithische Scripts | Funktionen, load() für Reuse |
MongoDB Shell Scripting ist mächtig, aber mit Power kommt Verantwortung. Scripts, die in Production laufen, können Daten korrumpieren, Performance beeinträchtigen oder Downtime verursachen wenn schlecht geschrieben. Die Best Practices – robustes Error-Handling, ausführliches Logging, Dry-Run-Support, Validation, Performance-Awareness – sind nicht optional für ernsthafte Automation. Der Unterschied zwischen einem Quick-Hack-Script und einem Production-Ready-Script ist die Disziplin, diese Patterns konsequent anzuwenden.