Abschnitt 12 · Lektion 3
Daten einfügen
Die Tabelle steht, jetzt kommen Zeilen hinein. Dabei lernst du drei Dinge, die zusammengehören: den Platzhalter, den Rückgabewert und die Transaktion.
Der Platzhalter und das Vorbereiten
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE buecher (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
jahr INTEGER
)`);
// Die Fragezeichen sind Platzhalter. Was hineinkommt, sagt run().
const einfuegen = db.prepare("INSERT INTO buecher (titel, jahr) VALUES (?, ?)");
const ergebnis = einfuegen.run("Node in der Praxis", 2024);
console.log("geschriebene Zeilen:", ergebnis.changes);
console.log("vergebene Kennung: ", ergebnis.lastInsertRowid);
// Kein id angegeben, und trotzdem steht eine drin.
const zweites = einfuegen.run("HTTP verstehen", 2023);
console.log("naechste Kennung: ", zweites.lastInsertRowid); db.prepare() nimmt eine Anweisung mit Fragezeichen darin und gibt dir etwas zurück, das du
ausführen kannst. Die Werte kommen erst bei run() dazu, in der Reihenfolge der Fragezeichen.
Warum man das so macht und nicht die Werte einfach in den Text schreibt, ist die wichtigste Sicherheitsregel des ganzen Kurses und bekommt in Lektion 12.5 eine eigene Lektion. Für heute reicht die mechanische Seite: Anweisung und Werte gehen getrennt zur Datenbank.
Was run() zurückgibt
Zwei Felder, und beide braucht man ständig.
changes sagt, wie viele Zeilen betroffen waren. Beim Einfügen ist das langweilig, da steht
immer 1. Beim Ändern und Löschen in Lektion 12.6 wird es zu der Angabe, an der du erkennst, ob es
die Zeile überhaupt gab, und damit zur Grundlage für den 404 aus Lektion 11.4.
lastInsertRowid ist die Kennung, die die Tabelle gerade vergeben hat. Genau die brauchst du für
die Location-Kopfzeile aus Lektion 11.3, und du bekommst sie geschenkt, statt sie zu zählen.
Die Kennung kommt von der Tabelle
Im Beispiel steht in keinem INSERT eine id, und trotzdem haben beide Zeilen eine. Das liegt an
id INTEGER PRIMARY KEY aus der letzten Lektion: Lässt man den Wert weg, setzt SQLite die nächste
freie Zahl ein.
Genau diese Regel aus Lektion 11.3 ist damit erfüllt, und zwar ohne dein Zutun: Die Kennung
vergibt der Server, nicht der Aufrufer. Der Zähler, den du dort als naechsteId im Speicher
gehalten hast, kann jetzt weg, und er hat den zusätzlichen Vorteil, dass er einen Neustart übersteht.
Nebenbei: Du wirst in anderen Anleitungen AUTOINCREMENT sehen. Das braucht man in SQLite fast nie.
Der Unterschied ist, dass AUTOINCREMENT verhindert, dass eine gelöschte Kennung später noch einmal
vergeben wird. Dafür legt SQLite eine zusätzliche Verwaltungstabelle an, und Arbeit kostet es auch.
Ohne guten Grund lässt man es weg.
Eine Anweisung, viele Werte
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE buecher (id INTEGER PRIMARY KEY, titel TEXT NOT NULL)");
const titel = ["Erstes", "Zweites", "Drittes", "Viertes", "Fuenftes"];
// Einmal vorbereitet, fuenfmal ausgefuehrt. Die Anweisung wird dabei nur
// einmal zerlegt und geprueft, nicht bei jedem Durchlauf neu.
const einfuegen = db.prepare("INSERT INTO buecher (titel) VALUES (?)");
db.exec("BEGIN");
for (const eintrag of titel) einfuegen.run(eintrag);
db.exec("COMMIT");
console.log("Zeilen:", db.prepare("SELECT count(*) AS n FROM buecher").get().n);
console.log("letzte Kennung:", db.prepare("SELECT max(id) AS m FROM buecher").get().m); prepare() einmal, run() fünfmal. Das ist nicht nur kürzer, sondern auch schneller: Die
Datenbank zerlegt und prüft die Anweisung einmal und führt danach nur noch aus. Bei fünf Zeilen ist
das egal, bei fünftausend nicht mehr.
Die Regel dazu ist einfach: prepare() gehört vor die Schleife, run() hinein. Wer beides in
die Schleife schreibt, bezahlt das Vorbereiten bei jedem Durchlauf noch einmal.
Warum das in eine Transaktion gehört
Um das BEGIN und COMMIT herum steht der eigentliche Grund, und der ist drastisch.
Ohne Transaktion behandelt SQLite jedes einzelne INSERT als abgeschlossenen Vorgang und wartet
jedes Mal darauf, dass die Platte das Geschriebene bestätigt. Dieses Warten ist der ganze Aufwand.
Gemessen auf einem gewöhnlichen Laptop: tausend Zeilen einzeln eingefügt brauchen rund 6300
Millisekunden, dieselben tausend Zeilen zwischen BEGIN und COMMIT rund 6. Das ist kein
Feinschliff, das ist Faktor tausend.
Das musst du mir nicht glauben. In der Aufgabe dieser Lektion liegt messen.js daneben, eine
Datei, die zum Kurs gehört und die du nicht anfasst. Sie fügt tausend Zeilen einmal einzeln und
einmal in einer Transaktion ein und misst beide Male die Zeit. Gestartet wird sie im Terminal mit
node messen.js, und der erste Durchgang dauert dann wirklich die paar Sekunden, die dort stehen.
Die Zahlen fallen auf jeder Maschine anders aus, das Verhältnis bleibt.
Es lohnt sich, dieses Verhältnis einmal gesehen zu haben, weil es die Erklärung für eine sehr verbreitete Beobachtung ist: „SQLite ist langsam” heißt in aller Regel „hier fehlt eine Transaktion”.
Ganz oder gar nicht
Der zweite Zweck einer Transaktion ist der, wegen dem sie so heißt.
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE konten (name TEXT PRIMARY KEY, stand INTEGER NOT NULL)");
db.exec("INSERT INTO konten VALUES ('Anna', 100), ('Bert', 100)");
const abbuchen = db.prepare("UPDATE konten SET stand = stand - ? WHERE name = ?");
const gutschreiben = db.prepare("UPDATE konten SET stand = stand + ? WHERE name = 'gibtsnicht'");
const staende = () =>
db
.prepare("SELECT name, stand FROM konten ORDER BY name")
.all()
.map((zeile) => `${zeile.name} ${zeile.stand}`)
.join(", ");
console.log("vorher: ", staende());
db.exec("BEGIN");
try {
abbuchen.run(30, "Anna");
// Die Gutschrift trifft niemanden. Wir merken es und drehen alles zurueck.
const gebucht = gutschreiben.run(30);
if (gebucht.changes === 0) throw new Error("Empfaenger gibt es nicht");
db.exec("COMMIT");
} catch (fehler) {
db.exec("ROLLBACK");
console.log("abgebrochen:", fehler.message);
}
console.log("nachher:", staende()); Anna wird abgebucht, die Gutschrift geht ins Leere, und am Ende hat Anna trotzdem ihre hundert. Das
ist die Zusage: Alles zwischen BEGIN und COMMIT passiert entweder vollständig oder gar nicht.
Drei Wörter, mehr braucht man nicht:
BEGIN öffnet die Klammer. COMMIT schließt sie und macht alles gültig. ROLLBACK verwirft alles
seit dem BEGIN, so als wäre nichts gewesen.
Der Fall im Beispiel ist bewusst einer, den kein Absturz auslöst: Die Gutschrift läuft technisch
fehlerfrei, sie trifft nur niemanden. Deshalb steht dort die Prüfung auf changes === 0. Eine
Transaktion dreht nicht von selbst zurück, wenn dein Programm etwas Sinnloses tut. Sie gibt dir nur
die Möglichkeit dazu, und du musst sie ergreifen.
Zum Mitnehmen
Tausend Zeilen einzeln geschrieben brauchen gemessene sechs Sekunden. Dieselben tausend Zeilen in einer Transaktion brauchen sechs Millisekunden.
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.