mitmario.dev

Die erste Tabelle

Node.js Sandbox 4 Min Lesezeit 3 BeispieleLektion 2 von 9

Eine SQLite-Datenbank aufzusetzen dauert eine Zeile. Es gibt keinen Dienst, den man startet, keine Zugangsdaten und keinen Port. Du sagst, wie die Datei heißen soll, und das war es.

Eine Datei, eine Tabelle

Eine Datei, eine Tabelle
import { DatabaseSync } from "node:sqlite";
import { mkdirSync, statSync } from "node:fs";

mkdirSync("daten", { recursive: true });

// Die Datei ist das einzige Argument. Gibt es sie noch nicht, entsteht
// sie. Den Ordner darum legt sie allerdings nicht an, deshalb die Zeile
// darueber.
const db = new DatabaseSync("daten/bibliothek.db");

db.exec(`CREATE TABLE IF NOT EXISTS buecher (
  id INTEGER PRIMARY KEY,
  titel TEXT NOT NULL,
  jahr INTEGER,
  ausgeliehen INTEGER NOT NULL DEFAULT 0
)`);

db.exec("INSERT INTO buecher (titel, jahr) VALUES ('Node in der Praxis', 2024)");

// Das Lesen ist der Stoff von 12.4, hier steht es nur, um die Zeile
// zeigen zu koennen, die eben entstanden ist.
const buch = db.prepare("SELECT id, titel, jahr, ausgeliehen FROM buecher").get();
db.close();

console.log("Datei:", statSync("daten/bibliothek.db").size, "Bytes");
console.log("Zeile:", JSON.stringify(buch));

DatabaseSync nimmt einen Dateipfad. Gibt es die Datei noch nicht, legt SQLite sie an. Was es nicht anlegt, ist der Ordner darum: ein fehlendes daten/ quittiert es mit unable to open database file, und das ist eine der Meldungen, bei denen man erst zehn Minuten die falsche Stelle sucht. Deshalb steht das mkdirSync davor.

db.exec() schickt SQL zur Datenbank und gibt nichts zurück. Es ist die richtige Wahl für alles, was etwas tut statt etwas zu liefern: Tabellen anlegen, Tabellen ändern, eine Transaktion öffnen. Alles, wo Zeilen zurückkommen sollen, läuft ab 12.3 über db.prepare().

Dass die Datenbank wirklich nur eine Datei ist, kannst du dir ansehen. Lass das Beispiel laufen und tipp danach im Terminal ls -l daten/. Dort steht bibliothek.db mit ihrer Größe. Und head -c 16 daten/bibliothek.db gibt dir die ersten sechzehn Zeichen daraus, nämlich SQLite format 3. So fängt jede SQLite-Datei an, und daran erkennt sie auch ein Programm, dem niemand den Dateinamen verraten hat. Die ganze Datei mit cat auszugeben lohnt dagegen nicht, dahinter kommen Bytes und keine Zeichen.

Was in einer Spalte stehen darf

SQLite kennt fünf Typen, und die Liste ist so kurz, dass man sie sich merken kann: INTEGER für ganze Zahlen, REAL für Kommazahlen, TEXT für Zeichenketten, BLOB für rohe Bytes und NULL für „hier steht nichts”.

Ein Datum gibt es nicht. Man speichert es als TEXT im Format 2026-08-09 oder als INTEGER mit den Sekunden seit 1970. Beides ist üblich, das erste ist lesbar, das zweite rechnet sich leichter.

Und jetzt die ehrliche Bemerkung, die andere Anleitungen gern weglassen: SQLite nimmt diese Typen nicht ganz so ernst wie andere Datenbanken. Wer einen Text in eine INTEGER-Spalte schreibt, darf das in vielen Fällen. Die Spalte ist eher eine Absichtserklärung als eine Schranke. Für dich heißt das nicht, dass du die Typen weglassen sollst, sondern dass du dich nicht auf sie verlassen darfst, um Unsinn draußen zu halten. Dafür ist die Prüfung an der Tür da, die du in 11.5 gebaut hast.

Die drei Zusätze, die man von Anfang an benutzt

PRIMARY KEY macht eine Spalte zum Ausweis der Zeile. Schreibst du id INTEGER PRIMARY KEY, bekommst du zwei Dinge geschenkt: Die Werte sind eindeutig, und wenn du beim Einfügen keinen angibst, zählt SQLite selbst hoch. Genau den Zähler, den du in Lektion 11.3 von Hand geführt hast, gibt es hier also fertig, und er überlebt den Neustart.

NOT NULL heißt, dass die Spalte nicht leer bleiben darf. Ein Buch ohne Titel ist kein Buch, also gehört es dorthin. Ein Buch ohne Jahr ist dagegen denkbar, und deshalb steht bei jahr nichts.

DEFAULT liefert den Wert, wenn beim Einfügen keiner mitkommt. ausgeliehen INTEGER NOT NULL DEFAULT 0 heißt: Die Spalte ist Pflicht, aber du musst sie nie mitschicken. Ohne die Vorgabe würde jedes Einfügen ohne ausgeliehen scheitern.

Der zweite Start

Zweimal gestartet
import { execSync } from "node:child_process";

// Zweimal derselbe Prozess. Bei einem Array im Speicher waere der zweite
// Lauf wieder bei null.
for (const lauf of [1, 2]) {
  console.log(`Lauf ${lauf}: ${execSync("node anlegen.js", { encoding: "utf8" }).trim()}`);
}

Zwei Läufe, zwei Zeilen in der Tabelle. Das ist der ganze Unterschied zu Abschnitt 11: Dort hieß das Ergebnis beide Male „1”, weil das Array mit dem Prozess starb.

Beachte, dass das Anlegen der Tabelle in jedem Lauf steht. Genau so gehört es sich. Deine Anwendung weiß beim Start nicht, ob sie zum ersten oder zum tausendsten Mal hochfährt, und sie soll in beiden Fällen laufen.

Und ohne IF NOT EXISTS

Dasselbe ohne IF NOT EXISTS
import { execSync } from "node:child_process";

// stdio: der Fehlerkanal des Kindes wird abgefangen statt durchgereicht,
// sonst stuende hier gleich der ganze Stacktrace.
const starte = () =>
  execSync("node ohne-schutz.js", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });

for (const lauf of [1, 2]) {
  try {
    console.log(`Lauf ${lauf}: ${starte().trim()}`);
  } catch (fehler) {
    const zeile = fehler.stderr.split("\n").find((z) => z.startsWith("Error:"));
    console.log(`Lauf ${lauf}: gescheitert, ${zeile}`);
  }
}

CREATE TABLE ohne den Zusatz heißt: „Leg diese Tabelle an, und wenn es sie schon gibt, ist das ein Fehler.” Beim ersten Start merkst du davon nichts, und ab dem zweiten kommt deine Anwendung nicht mehr hoch.

Das ist die häufigste Panne in dieser Lektion, und sie ist auf dem eigenen Rechner besonders leicht zu übersehen: Solange du zwischendurch die Datei löschst, geht immer alles gut.

Drei Wörter, ein Problem weniger. Sie gehören an jedes CREATE TABLE, das beim Start deiner Anwendung läuft.

Alles hier ist synchron

Zum Schluss ein Punkt, der aus Lektion 6.5 folgt und den man kennen sollte, bevor man ihn im Betrieb entdeckt: node:sqlite arbeitet synchron. Es gibt kein await. Jede Abfrage hält die Ereignisschleife an, bis sie fertig ist.

Für kleine Abfragen ist das nicht nur in Ordnung, sondern sogar schnell: Es fällt keine Netzwerkrunde an und kein Wechsel in einen anderen Prozess, die Datei liegt direkt daneben. Eine Abfrage über eine Million Zeilen ist etwas anderes. Solange sie läuft, nimmt dein Server keine einzige andere Anfrage an, und das ist genau die Blockade, die du in 6.5 gemessen hast.

Der Name sagt es übrigens von sich aus: DatabaseSync heißt so, weil es auch ein asynchrones Gegenstück geben könnte. Heute gibt es das noch nicht.

Zum Mitnehmen

Ein Dateiname genügt, und die Datenbank ist da. Der wichtigste Handgriff der Lektion sind drei Wörter: IF NOT EXISTS.

Jetzt du

Basis Konto, kostenlos

Zu 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.