Zum Inhalt
wannacode

Dokumentendatenbanken

Kurz gesagt: Eine Dokumentendatenbank wie MongoDB speichert jeden Datensatz als Dokument mit verschachtelten Feldern und Arrays, ohne ein einheitliches Schema für alle Datensätze.

Problem

Produkte, die sich nicht gleichen

In der Tabelle products hat jedes Produkt dieselben Spalten: Name, Kategorie, Preis, Bestand. Der Shop will mehr zeigen. Das Gewicht und die Allergene beim Brot, die Herkunft und die Röstung beim Kaffee, das Volumen beim Spülmittel. Die Tabelle lässt zwei schlechte Wahlmöglichkeiten. Die erste sind dutzende Spalten, die bei den meisten Produkten NULL sind. Die zweite ist eine eigene Tabelle „Eigenschaft, Wert“, in der alles Text ist und jedes Lesen ein weiteres JOIN bedeutet.

Eine Bestellung hat ein ähnliches Problem. Um sie dem Kunden zu zeigen, setzt du sie aus vier Tabellen zusammen:

SQLDatenbank: Shop

Die Seite zeigt die Bestellung immer als Ganzes: den Kunden, die Positionen, die Preise. Und die Datenbank setzt sie jedes Mal neu zusammen.

Die Idee

Das Dokument

Eine Dokumentendatenbank hält jeden Datensatz als Dokument: ein Objekt mit Feldern, wie JSON. Die Dokumente liegen in einer Collection, die einer Tabelle ähnelt, aber die Felder nicht vorgibt. Jedes Dokument hat seine eigenen. Anders als eine Key-Value-Datenbank sieht eine Dokumentendatenbank in den Wert hinein und sucht nach seinen Feldern.

Das Feld ist ein Simulator mit den MongoDB-Befehlen. Ihre Konsole heißt mongosh, und die Befehle beginnen mit db. und dem Namen der Collection:

Dokumentendatenbank (wie MongoDB)Simulator

Das Brot hat weight_g und allergens, der Kaffee origin und roast, das Spülmittel volume_ml. Ein Wert kann ein Array (allergens) oder ein anderes Dokument sein. _id ist der Primärschlüssel: Jedes Dokument hat einen, und er wiederholt sich nicht.

Genauer, für Neugierige

Gibst du keine _id an, fügt MongoDB selbst eine ObjectId('…') hinzu: 12 Bytes, die den Zeitpunkt der Erstellung und einen zufälligen Teil enthalten. Der Simulator vergibt sie der Reihe nach (ObjectId('6720a0000000000000000001'), dann …0002), damit die Ausgabe jedes Mal gleich ist. Echte laufen nicht so. Ohne sort() liefert der Simulator die Dokumente in der Reihenfolge, in der sie hineingekommen sind. MongoDB verspricht ohne sort() überhaupt keine Reihenfolge. Die Ausgabe ist wie in mongosh 2: Text steht in einfachen Anführungszeichen, und lange Dokumente werden mit einem Feld pro Zeile umbrochen.

Probier es aus

In den Dokumenten suchen

find(filter, projection) sucht. Der Filter ist ein Dokument mit den Feldern, die passen müssen. { price: { $lt: 3 } } heißt „Preis unter 3“: $lt ist „kleiner als“, $lte „kleiner oder gleich“, $gt und $gte „größer“, $ne „ungleich“, $in „eins der aufgezählten“. $exists fragt, ob das Feld überhaupt da ist. Die Projektion sagt, welche Felder zurückkommen: 1 heißt „ja“, und _id: 0 lässt _id weg, die sonst immer mitkommt. .sort({ price: 1 }) sortiert aufsteigend (-1 ist absteigend), und countDocuments zählt:

Dokumentendatenbank (wie MongoDB)Simulator

Der Filter { allergens: 'Milch' } findet die Dokumente, deren Array 'Milch' enthält. Vorsicht: { allergens: ['Milch'] } sucht genau dieses Array, mit einem Element und sonst nichts.

Zeige die Namen der Produkte, die Gluten enthalten, ohne _id.

Dokumentendatenbank (wie MongoDB)Simulator

Sieh es dir an

Ein Tippfehler im Feldnamen

Die Freiheit hat ihren Preis. Eine Tabelle kennt die Namen ihrer Spalten: Ein UPDATE einer Spalte prise, die es nicht gibt, liefert einen Fehler. Eine Collection kennt keine Namen. Der Kaffee wird billiger, 12,90 €, aber im Namen des Feldes steckt ein Tippfehler:

Dokumentendatenbank (wie MongoDB)Simulator

Die Datenbank antwortet modifiedCount: 1 und meldet keinen Fehler. Sie hat ein neues Feld prise hinzugefügt, und price ist weiterhin 14.9. Deshalb taucht der Kaffee nicht unter den Produkten unter 13 € auf. Ein solcher Fehler zeigt sich erst, wenn jemand den alten Preis auf der Seite bemerkt.

Deshalb bewacht das Programm, das in eine Dokumentendatenbank schreibt, die Form selbst: eine Klasse oder ein Schema im Code für jede Art von Dokument. MongoDB kann die Dokumente beim Schreiben auch gegen Regeln für die Collection prüfen.

Genauer, für Neugierige

Die Regeln werden mit db.createCollection('products', { validator: { $jsonSchema: … } }) oder später mit dem Befehl collMod festgelegt. Sie sagen, welche Felder Pflicht sind und von welchem Typ. Der Simulator hat keine solche Prüfung.

Die Idee

Einbetten oder Referenzieren

Die Bestellung vom Anfang kann ein einziges Dokument sein, mit dem Kunden und den Positionen darin. Das heißt Einbetten:

Dokumentendatenbank (wie MongoDB)Simulator

findOne liefert es ganz in einem Lesevorgang, ohne JOIN. Ein Feld im eingebetteten Dokument erreichst du mit einem Punkt: 'customer.city'. Der Pfad steht in Anführungszeichen, weil er einen Punkt hat. Dasselbe funktioniert durch ein Array hindurch. In diesem Feld hat die Collection orders schon die Bestellungen 53–60 des Shops, und 'items.product_id': 1 findet die, in denen irgendeine Position das Weißbrot ist:

Dokumentendatenbank (wie MongoDB)Simulator

Der Name und der Preis in den Positionen sind Kopien, und das ist Absicht: Die Bestellung behält den Preis vom Tag des Kaufs, wie unit_price in order_items. Der Name des Kunden ist auch eine Kopie. Ändert Maria ihren Namen, behalten die alten Bestellungen den alten. Für eine Bestellung ist das richtig, aber das Profil des Kunden lebt in einer eigenen Collection, und die Bestellung trägt nur customer.id.

Du bettest ein, wenn der Teil zum Dokument gehört, mit ihm gelesen wird und klein ist: Eine Bestellung hat ein paar Positionen, nicht tausende. Du nutzt eine Referenz, wenn etwas vielen Dokumenten gemeinsam gehört oder ohne Ende wächst. So sind die Bewertungen des Kaffees: Es gibt viele, sie werden zu beliebiger Zeit geschrieben, und jede hat ihren eigenen Autor. Sie liegen in einer eigenen Collection, und product_id zeigt auf das Produkt, wie ein Fremdschlüssel:

Dokumentendatenbank (wie MongoDB)Simulator

Einbetten ist Denormalisierung: schnelleres Lesen im Tausch gegen Kopien, die auseinanderlaufen können.

Genauer, für Neugierige

Ein Dokument in MongoDB ist höchstens 16 MB groß, ein Array, das ohne Ende wächst, passt also früher oder später nicht mehr hinein. Referenzen werden in aggregate mit der Stufe $lookup verbunden (mit from, localField, foreignField und as), die so etwas wie ein LEFT JOIN macht. Der Simulator kennt sie nur in dieser Form. Wenn du in jeder Abfrage $lookup brauchst, sind deine Daten wahrscheinlich relational.

Probier es aus

Änderungen im Dokument

updateOne(filter, change) ändert das erste Dokument, das es findet, und updateMany alle. Die Änderung ist ein Operator: $set setzt einen Wert (und legt das Feld an, wenn es fehlt), $inc addiert zu einer Zahl, $push fügt einem Array etwas hinzu, $unset entfernt ein Feld. Dieses Feld hat wieder die Bestellungen 53–60. Bestellung 60 ist geliefert, Butter kommt dazu, und das Weißbrot wird drei:

Dokumentendatenbank (wie MongoDB)Simulator

'items.$.qty' heißt „die qty der Position, die der Filter gefunden hat“. Der Filter 'items.product_id': 1 findet die Position des Weißbrots, und $ ist ihr Platz im Array. Das Dokument ändert sich ganz auf einmal: Niemand kann die Bestellung halb geändert lesen.

Ohne Operator weigert sich mongosh: updateOne({ _id: 60 }, { status: 'delivered' }) liefert den Fehler Update document requires atomic operators. Und $set auf das ganze Array items ersetzt es, und die alten Positionen sind weg.

Viktoria (Bestellung 58) will noch ein Vollkornbrot (Produkt 2) und zwei Joghurts: { product_id: 6, name: 'Joghurt', qty: 2, price: 1.1 }.

Dokumentendatenbank (wie MongoDB)Simulator

Die Idee

Aggregation

Fragen nach „wie viel“ und „wie viel pro“ (Umsatz pro Stadt, meistverkaufte Produkte) schreibt man mit aggregate: einem Array von Stufen, die die Dokumente nacheinander durchlaufen. Das ist eine Pipeline:

  • $match filtert, wie WHERE;
  • $unwind zerlegt die Bestellung in ein Dokument für jede Position in items;
  • $group sammelt die Dokumente nach _id in Gruppen, wie GROUP BY, mit $sum, $avg, $min und $max;
  • $project behält oder berechnet Felder, und $sort und $limit ordnen und schneiden ab.

Der Umsatz pro Stadt ohne die stornierten Bestellungen, und danach die Anzahl der Bestellungen:

Dokumentendatenbank (wie MongoDB)Simulator

{ $sum: 1 } zählt die Dokumente in der Gruppe, wie COUNT(*). Ohne $unwind würden die Gruppen Bestellungen addieren, nicht Positionen. $round rundet auf den Cent: Sonst kommt eine Summe von Bruchzahlen als 22.799999999999997 statt 22,80 € heraus, wie bei allen Gleitkommazahlen.

Genauer, für Neugierige

MongoDB verspricht nach $group keine Reihenfolge der Gruppen, deshalb folgt danach ein $sort, und bei gleichen Werten auch ein zweiter Schlüssel (_id: 1). Der Simulator liefert die Gruppen in der Reihenfolge, in der sie aufgetaucht sind. Er addiert Bruchzahlen ein wenig anders als MongoDB und unterscheidet sich manchmal in der letzten Stelle. Nach $round ergeben beide dasselbe.

Übung

Versand, Allergene und Bestseller

Die Felder der Aufgaben haben die Bestellungen 53–60 und die Produkte vom Anfang der Lektion.

1. Der Kurier kommt für die bezahlten Bestellungen.

Ändere den Status aller Bestellungen mit status: 'paid' auf 'shipped'.

Dokumentendatenbank (wie MongoDB)Simulator

2. Es stellt sich heraus, dass im Butterbrötchen auch Eier stecken.

Füge 'Eier' zu den Allergenen des Butterbrötchens (Produkt 3) hinzu.

Dokumentendatenbank (wie MongoDB)Simulator

3. Das Lager will wissen, was sich am meisten verkauft.

Welche Produkte haben sich 5 Stück oder öfter verkauft (ohne die stornierten Bestellungen)? Zeige sie vom Bestseller abwärts, und bei gleicher Stückzahl nach Namen.

Dokumentendatenbank (wie MongoDB)Simulator

Die Idee

Wann eine Dokumentendatenbank, wann nicht

Eine Dokumentendatenbank ist gut, wenn ein Datensatz als Ganzes gelesen und geschrieben wird und sich seine Form ändert: ein Katalog verschiedener Produkte, Profile und Einstellungen, der Inhalt einer Seite, Ereignisse aus einer App, eine Bestellung, die auf einmal angezeigt wird.

Sie ist eine schlechte Wahl, wenn die Daten in alle Richtungen verbunden sind. Der Shop fragt „welcher Kunde hat was gekauft“, „welche Produkte werden zusammen gekauft“, „Umsatz pro Kategorie und Monat“. Jede dieser Fragen geht durch Kunden, Bestellungen und Produkte zugleich. In SQL sind das JOINs über normalisierte Tabellen, in denen jede Tatsache einmal geschrieben steht. Hier sind es Kopien, die gleich gehalten werden müssen, oder ein $lookup in jeder Abfrage.

Dasselbe gilt für eine Operation, die viele Dokumente berührt: eine Zahlung, die den Bestand von fünf Produkten senkt und die Bestellung speichert. Die Änderung eines Dokuments ist alles oder nichts. Mehrere Dokumente brauchen eine Transaktion, die komplexer und langsamer ist als ein einzelnes Schreiben.

Genauer, für Neugierige

Dokumentendatenbanken sind MongoDB (die am weitesten verbreitete, mit mongosh und aggregate wie im Simulator), Couchbase, Apache CouchDB, Firestore in Google Cloud und Amazon DocumentDB, das die MongoDB-Befehle versteht. MongoDB hat Transaktionen über viele Dokumente seit Version 4.0. PostgreSQL hat den Typ jsonb: eine Spalte mit einem Dokument in einer gewöhnlichen Tabelle, mit Indizes und Operatoren für ihre Felder. Das ist oft der gute Mittelweg: Tabellen für die verbundenen Daten und jsonb für die verschiedenen Eigenschaften der Produkte.

Test

Teste dich

1. Der Befehl db.products.updateOne({ _id: 17 }, { $set: { prise: 12.9 } }) liefert modifiedCount: 1. Was ist passiert?
2. Was lässt sich gut in das Dokument einer Bestellung einbetten?
3. Was findet db.products.find({ allergens: 'Milch' })?
4. Eine Zahlung senkt den Bestand von fünf Produkten und speichert die Bestellung. Warum ist hier eine Dokumentendatenbank nicht die beste Wahl?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·