Abschnitt 12 · Lektion 1
Warum eine Datenbank?
Die Notizverwaltung aus Abschnitt 3 speichert in einer JSON-Datei, und das war völlig richtig so. Ein Programm, ein Lauf, eine Datei. Auf einem Server gilt diese Rechnung nicht mehr, denn dort läuft dein Code nicht einmal, sondern gleichzeitig für jeden, der gerade etwas will.
Zwei Anfragen, ein verlorener Eintrag
import { mkdir, writeFile, readFile, rename } from "node:fs/promises";
await mkdir("daten", { recursive: true });
await writeFile("daten/liste.json", JSON.stringify(["Bestellung 1"]));
const lies = async () => JSON.parse(await readFile("daten/liste.json", "utf8"));
// So sauber, wie es mit einer Datei ueberhaupt geht: geschrieben wird in
// eine Zwischendatei, danach umbenannt. Ein halb geschriebenes
// liste.json kann es damit nicht geben.
const schreibe = async (liste, wer) => {
await writeFile(`daten/${wer}.tmp`, JSON.stringify(liste));
await rename(`daten/${wer}.tmp`, "daten/liste.json");
};
// Zwei Anfragen treffen gleichzeitig ein. Beide lesen denselben Stand.
const [standAnna, standBert] = await Promise.all([lies(), lies()]);
standAnna.push("Bestellung von Anna");
standBert.push("Bestellung von Bert");
// Und beide schreiben ihn zurueck.
await schreibe(standAnna, "anna");
await schreibe(standBert, "bert");
console.log("abgeschickt: Anna, Bert");
console.log("in der Datei:", (await lies()).join(" | ")); Anna und Bert schicken im selben Moment eine Bestellung ab. Danach steht Bert in der Datei und Anna nicht. Kein Absturz, keine Meldung, kein Eintrag im Protokoll. Die Bestellung ist einfach weg.
Das liegt nicht an schlampigem Code. Schau dir das Schreiben im Beispiel an: erst in eine
Zwischendatei, dann umbenennen. Ein rename innerhalb desselben Dateisystems ist unteilbar, ein
Leser sieht also immer entweder den alten oder den neuen Stand und nie etwas dazwischen. Sauberer
geht es mit einer Datei kaum.
Der Fehler steckt nicht im Schreiben, sondern in der Reihenfolge. Wer eine Datei als Speicher benutzt, macht immer drei Schritte: lesen, ändern, zurückschreiben. Zwischen dem Lesen und dem Zurückschreiben liegt eine Lücke, und in dieser Lücke arbeitet die andere Anfrage mit einem Stand, der schon veraltet ist. Ihr Zurückschreiben überschreibt dann alles, was in der Zwischenzeit passiert ist.
Auf deinem Rechner triffst du das fast nie. Du klickst einmal, wartest, klickst wieder. Sobald zwei Leute gleichzeitig auf derselben Seite sind, ist es der Normalfall.
Dieselbe Sache mit einer Tabelle
import { DatabaseSync } from "node:sqlite";
import { mkdirSync } from "node:fs";
mkdirSync("daten", { recursive: true });
// Einmal aufsetzen, danach zwei getrennte Verbindungen auf dieselbe
// Datei. Genau das tun zwei gleichzeitige Anfragen auf einem Server auch.
const aufbau = new DatabaseSync("daten/bestellungen.db");
aufbau.exec(`CREATE TABLE IF NOT EXISTS bestellungen (
id INTEGER PRIMARY KEY,
text TEXT NOT NULL
)`);
aufbau.prepare("INSERT INTO bestellungen (text) VALUES (?)").run("Bestellung 1");
aufbau.close();
const anna = new DatabaseSync("daten/bestellungen.db");
const bert = new DatabaseSync("daten/bestellungen.db");
// Niemand liest den Bestand, niemand schreibt ihn zurueck. Jeder haengt
// eine Zeile an, und mehr passiert nicht.
anna.prepare("INSERT INTO bestellungen (text) VALUES (?)").run("Bestellung von Anna");
bert.prepare("INSERT INTO bestellungen (text) VALUES (?)").run("Bestellung von Bert");
anna.close();
bert.close();
const db = new DatabaseSync("daten/bestellungen.db");
const zeilen = db.prepare("SELECT text FROM bestellungen ORDER BY id").all();
db.close();
console.log("abgeschickt: Anna, Bert");
console.log("in der Tabelle:", zeilen.map((zeile) => zeile.text).join(" | ")); Beide Bestellungen sind da. Und das ist keine Zauberei, sondern die Folge davon, dass die drei Schritte auf einen zusammengeschrumpft sind: es wird nichts mehr gelesen und nichts mehr zurückgeschrieben, sondern eine Zeile angehängt. Ohne Lücke gibt es nichts, in das die andere Anfrage hineinfallen könnte.
Dazu kommt, dass die Datenbank die gleichzeitigen Zugriffe selbst ordnet. Zwei Schreibvorgänge auf dieselbe Datei laufen nacheinander ab, auch wenn sie gleichzeitig ankommen, und darum musst du dich nicht kümmern. Genau dafür gibt es sie.
Suchen, ohne alles zu lesen
Der zweite Grund fällt später auf als der erste, dafür wächst er mit.
import { DatabaseSync } from "node:sqlite";
import { mkdirSync, writeFileSync, statSync, readFileSync } from "node:fs";
mkdirSync("daten", { recursive: true });
// Fuenftausend Bestellungen, einmal als JSON-Datei und einmal als Tabelle.
const bestellungen = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
kunde: `Kunde ${i + 1}`,
betrag: (i % 90) + 10,
}));
writeFileSync("daten/bestellungen.json", JSON.stringify(bestellungen));
const db = new DatabaseSync("daten/bestellungen.db");
db.exec("CREATE TABLE IF NOT EXISTS bestellungen (id INTEGER PRIMARY KEY, kunde TEXT NOT NULL, betrag INTEGER NOT NULL)");
db.exec("BEGIN");
const einfuegen = db.prepare("INSERT INTO bestellungen (id, kunde, betrag) VALUES (?, ?, ?)");
for (const eintrag of bestellungen) einfuegen.run(eintrag.id, eintrag.kunde, eintrag.betrag);
db.exec("COMMIT");
// Weg 1: die ganze Datei lesen, die ganze Liste zerlegen, dann suchen.
const liste = JSON.parse(readFileSync("daten/bestellungen.json", "utf8"));
let angesehen = 0;
const ausJson = liste.find((eintrag) => {
angesehen += 1;
return eintrag.id === 4711;
});
const kb = Math.round(statSync("daten/bestellungen.json").size / 1024);
console.log(`JSON: ${kb} KB gelesen, ${liste.length} Eintraege zerlegt, ${angesehen} angesehen`);
console.log(` gefunden: ${ausJson.kunde}`);
// Weg 2: die Datenbank fragen.
const ausDb = db.prepare("SELECT kunde FROM bestellungen WHERE id = ?").get(4711);
db.close();
console.log(`SQLite: eine Abfrage, eine Zeile zurueck`);
console.log(` gefunden: ${ausDb.kunde}`); Um eine einzige Bestellung aus der JSON-Datei zu holen, liest dein Programm 218 KB von der Platte, baut daraus 5000 Objekte im Arbeitsspeicher auf und sieht sich 4711 davon an. Bei fünftausend Einträgen merkt man das kaum. Bei fünfhunderttausend ist es der Unterschied zwischen einer Seite, die sofort da ist, und einer, bei der der Ventilator angeht.
Die Datenbank tut dasselbe in einem Schritt und gibt dir eine Zeile zurück. Sie kann das, weil sie weiß, wo die Zeile liegt, statt sie zu suchen. Wie das genau funktioniert, ist eine eigene Geschichte; für heute reicht: filtern lässt du die Datenbank machen, nicht JavaScript.
Und das Versprechen, das man erst später vermisst
Es gibt einen dritten Punkt, der hier nur genannt und in Lektion 12.3 eingelöst wird. Eine Datenbank kann mehrere Änderungen zu einem Paket zusammenfassen, das entweder ganz oder gar nicht passiert. Wenn du bei einer Überweisung von einem Konto abbuchst und dem anderen gutschreibst, willst du genau das, und mit zwei Schreibvorgängen auf eine Datei bekommst du es nicht.
Warum SQLite und wo ihre Grenze liegt
SQLite ist eine Datei. Kein Serverprozess, den man startet, keine Zugangsdaten, kein Port. Du gibst einen Dateinamen an, und wenn die Datei noch nicht da ist, entsteht sie. Kopieren heißt: die Datei kopieren. Sichern heißt: die Datei sichern.
In Node brauchst du dafür nichts zu installieren. node:sqlite ist eingebaut, so wie node:fs und
node:http. Kein Paket, keine Version, die veraltet, nichts, was in Abschnitt 5 zu bewerten wäre.
Die Grenze liegt beim gleichzeitigen Schreiben. Lesen können beliebig viele Prozesse zugleich, geschrieben wird nacheinander. Für eine Anwendung mit ein paar hundert oder tausend Nutzern reicht das mit großem Abstand, und viele Seiten, die du täglich benutzt, kämen damit aus. Wenn irgendwann mehrere Server gleichzeitig in dieselben Daten schreiben sollen, wechselt man zu PostgreSQL. Das ist dann ein anderer Verbindungsaufbau und fast dasselbe SQL, und es ist kein Grund, heute damit anzufangen.
Zum Mitnehmen
Eine JSON-Datei als Speicher ist kein Anfängerfehler. Sie hat nur eine Grenze, und die liegt genau da, wo zwei Anfragen gleichzeitig ankommen.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.