Abschnitt 12 · Lektion 6
Ändern und löschen
Zwei Anweisungen, ein gemeinsames Muster und eine Warnung, die man einmal gelesen haben sollte, bevor man sie erlebt.
Ändern und löschen
export const AUFBAU = `CREATE TABLE buecher (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
jahr INTEGER,
ausgeliehen INTEGER NOT NULL DEFAULT 0
);
INSERT INTO buecher (titel, jahr) VALUES
('Node in der Praxis', 2020),
('JavaScript von vorn', 2019),
('HTTP verstehen', 2017),
('Datenbanken im Alltag', 2014),
('Die lange Nacht des Debuggens', 2022);`; import { DatabaseSync } from "node:sqlite";
import { AUFBAU } from "./fixture.js";
const db = new DatabaseSync(":memory:");
db.exec(AUFBAU);
const aendern = db.prepare("UPDATE buecher SET jahr = ? WHERE id = ?");
const loeschen = db.prepare("DELETE FROM buecher WHERE id = ?");
console.log("Jahr von Buch 3 aendern:", aendern.run(2018, 3).changes, "Zeile(n)");
console.log("Jahr von Buch 99 aendern:", aendern.run(2018, 99).changes, "Zeile(n)");
console.log("Buch 5 loeschen:", loeschen.run(5).changes, "Zeile(n)");
console.log("Buch 99 loeschen:", loeschen.run(99).changes, "Zeile(n)");
console.log("Bestand:", db.prepare("SELECT count(*) AS n FROM buecher").get().n); UPDATE buecher SET jahr = ? WHERE id = ? und DELETE FROM buecher WHERE id = ?. Beide haben
denselben Aufbau: was passieren soll, und darunter, für welche Zeilen.
Beide gibst du mit run() aus, denn beide liefern keine Zeilen zurück, sondern eine Bilanz.
changes ist die Antwort auf eine andere Frage
Achte auf die beiden Anweisungen, die sich auf Buch 99 beziehen. Das gibt es nicht, und trotzdem ist nichts abgestürzt. Eine Änderung, die keine Zeile trifft, ist in SQL kein Fehler. Die Anweisung war korrekt, sie hat nur nichts gefunden, worauf sie zutreffen könnte.
Deshalb gibt es changes. Es ist die einzige Stelle, an der du den Unterschied zwischen den beiden
Fällen erfährst:
changes: 1 heißt „gefunden und geändert”. changes: 0 heißt „gab es nicht”.
Damit ist die Frage aus Lektion 11.4 beantwortet, wie man beim Ändern und Löschen zu einem
ehrlichen 404 kommt. In Abschnitt 11 hast du dafür find und findIndex benutzt und geprüft, ob
undefined oder -1 herauskam. Hier ist es changes === 0, und du brauchst dafür keine zweite
Abfrage.
Ein vergessenes WHERE
export const AUFBAU = `CREATE TABLE buecher (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
jahr INTEGER,
ausgeliehen INTEGER NOT NULL DEFAULT 0
);
INSERT INTO buecher (titel, jahr) VALUES
('Node in der Praxis', 2020),
('JavaScript von vorn', 2019),
('HTTP verstehen', 2017),
('Datenbanken im Alltag', 2014),
('Die lange Nacht des Debuggens', 2022);`; import { DatabaseSync } from "node:sqlite";
import { AUFBAU } from "./fixture.js";
const db = new DatabaseSync(":memory:");
db.exec(AUFBAU);
const zeige = (was) =>
console.log(
was,
db.prepare("SELECT id, ausgeliehen FROM buecher ORDER BY id").all().map((z) => `${z.id}:${z.ausgeliehen}`).join(" "),
);
zeige("vorher: ");
// Gemeint war: dieses eine Buch. Getroffen sind alle.
const vergessen = db.prepare("UPDATE buecher SET ausgeliehen = 1");
console.log("betroffene Zeilen:", vergessen.run().changes);
zeige("nachher:"); Fünf Zeilen betroffen, wo eine gemeint war. Es gibt keine Rückfrage, keine Warnung und nichts, was das aufhält.
Ein UPDATE ohne WHERE gilt für die ganze Tabelle. Ein DELETE ohne WHERE leert sie. Das
ist kein Versehen im Entwurf von SQL, es ist manchmal genau das, was man will. Nur eben selten.
Drei Gewohnheiten, die davor schützen:
Schreib die Anweisung von hinten. Erst WHERE id = ?, dann davor das UPDATE ... SET .... Dann
kann das WHERE gar nicht erst fehlen.
Sieh dir changes an. Eine Zahl, die viel größer ist als erwartet, ist die Meldung, die dir sonst
niemand gibt.
Und bei etwas, das du nicht rückgängig machen kannst: erst als SELECT mit demselben WHERE
schreiben, ansehen, und dann das SELECT durch das DELETE ersetzen.
Nur die Felder setzen, die geschickt wurden
In Lektion 11.4 war der Unterschied zwischen PUT und PATCH das Thema: PUT ersetzt den ganzen
Eintrag, PATCH ändert nur, was mitkommt. Auf der SQL-Seite heißt das, dass in SET nur die
Spalten stehen dürfen, für die auch ein Wert geschickt wurde.
Der naheliegende Weg ist ein UPDATE, das alle Spalten setzt und für die fehlenden den alten Wert
einträgt. Das braucht aber ein vorheriges Lesen und hat eine hässliche Lücke: Zwischen deinem Lesen
und deinem Schreiben kann jemand anders die Zeile geändert haben, und du schreibst seinen Stand mit
alten Werten wieder zu. Das ist derselbe verlorene Schreibvorgang wie in Lektion 12.1, nur eine
Etage höher.
Der saubere Weg ist, die SET-Liste aus den vorhandenen Feldern zusammenzusetzen und die Werte als
Parameter zu übergeben. Die Spaltennamen kommen dabei aus deiner eigenen festen Liste, niemals
aus den Schlüsseln des empfangenen Objekts, und damit gilt weiterhin die Regel aus 12.5. In
Lektion 12.8 baust du genau das für die Bücher-API.
Anlegen oder ändern in einem
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE zaehler (
seite TEXT PRIMARY KEY,
aufrufe INTEGER NOT NULL DEFAULT 0
)`);
// Anlegen, und falls es die Zeile schon gibt, stattdessen hochzaehlen.
const zaehle = db.prepare(`INSERT INTO zaehler (seite, aufrufe) VALUES (?, 1)
ON CONFLICT(seite) DO UPDATE SET aufrufe = aufrufe + 1`);
for (const seite of ["/start", "/kurse", "/start", "/start"]) zaehle.run(seite);
for (const zeile of db.prepare("SELECT seite, aufrufe FROM zaehler ORDER BY seite").all()) {
console.log(`${zeile.seite}: ${zeile.aufrufe}`);
} Manchmal weiß man nicht, ob es die Zeile schon gibt, und es ist auch egal: Zähler, Einstellungen,
ein Merkzettel je Nutzer. Für diesen Fall gibt es ON CONFLICT ... DO UPDATE, gesprochen UPSERT.
Der Sinn ist nicht die Kürze, sondern dass es ein Vorgang ist. Ein SELECT, gefolgt von einem
INSERT oder UPDATE je nach Ergebnis, hat wieder die Lücke dazwischen. Der UPSERT hat sie nicht.
Zum Mitnehmen
Ein UPDATE ohne WHERE gilt für alle Zeilen, und es fragt nicht nach. Das ist der Fehler, den fast jeder genau einmal macht.
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.