11. Persistenz
Persistenz
Abschnitt betitelt „Persistenz“Die Render-Farm speichert ihre Jobs bisher in einer JSON-Datei; das war ein ehrlicher Zwischenstand, und für kleine Werkzeuge ist es sogar eine dauerhaft vertretbare Lösung. Aber die Datei hat strukturelle Grenzen, und wer sie kennt, weiß, wann eine Datenbank fällig ist. Dieses Kapitel zieht die Datenzugriffsschicht auf SQLite um, ohne dass Service oder Oberfläche eine Zeile ändern (der versprochene Beweis des Architekturkapitels), und behandelt den Datenaustausch zwischen Anwendungen über strukturierte Formate, wie ihn der Lehrplan verlangt.
Das SQL-Handwerk selbst (SELECT, INSERT, Joins, Schema-Entwurf) ist Stoff des Gegenstands INSY und wird hier vorausgesetzt; dieses Kapitel behandelt die Anbindung: Wie kommt eine Datenbank in ein TypeScript-Projekt, und wo wohnt sie in der Architektur?
Warum die Datei nicht reicht
Abschnitt betitelt „Warum die Datei nicht reicht“Drei Grenzen der JSON-Datei, jede ein realer Fehlermodus:
- Gleichzeitige Zugriffe. Zwei Anfragen treffen fast zeitgleich ein; beide lesen die Datei, beide schreiben sie zurück, und die zweite überschreibt die Änderung der ersten. Ein klassisches Lost Update, dieselbe Fehlerfamilie wie die Race Conditions aus dem Nebenläufigkeitskapitel, nur auf der Datenebene.
- Suchen und Filtern. “Alle fehlgeschlagenen Jobs der letzten Woche” heißt bei der Datei: alles laden, alles durchgehen. Bei tausend Einträgen unelegant, bei einer Million unbenutzbar. Datenbanken beantworten solche Fragen über Indizes, ohne alles zu lesen.
- Teilweise Schreibvorgänge. Stürzt der Prozess mitten im Schreiben ab, hinterlässt er eine halbe, kaputte Datei; alle Daten sind verloren. Datenbanken garantieren, dass Änderungen ganz oder gar nicht passieren.
SQLite im Projekt
Abschnitt betitelt „SQLite im Projekt“SQLite ist die Datenbank für genau diese Lücke: eine vollwertige SQL-Datenbank ohne eigenen Server, gespeichert in einer einzigen Datei im Projekt. Kein Dienst, keine Zugangsdaten, keine Installation für Mitschüler, die das Repository klonen; für Schulprojekte und für erstaunlich viel Produktivsoftware ist SQLite die richtige Wahl. Als Treiber verwenden wir better-sqlite3, dessen API angenehm direkt ist:
npm install better-sqlite3npm install --save-dev @types/better-sqlite3Das Repository, dritte Implementierung des bekannten Interfaces:
import Database from "better-sqlite3";import type { JobRepository } from "./job-repository";import type { Job } from "@/lib/types";
export class SqliteJobRepository implements JobRepository { private db: Database.Database;
constructor(path: string) { this.db = new Database(path); this.db.exec(` CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, preset TEXT NOT NULL, state TEXT NOT NULL, progress REAL NOT NULL, created_at TEXT NOT NULL ) `); }
async findAll(): Promise<Job[]> { const rows = this.db.prepare("SELECT * FROM jobs ORDER BY id").all(); return (rows as JobRow[]).map(toJob); }
async findById(id: number): Promise<Job | null> { const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(id); return row ? toJob(row as JobRow) : null; }
async insert(job: Omit<Job, "id">): Promise<Job> { const result = this.db.prepare(` INSERT INTO jobs (filename, preset, state, progress, created_at) VALUES (?, ?, ?, ?, ?) `).run(job.filename, job.preset, job.state, job.progress, job.createdAt); return { ...job, id: Number(result.lastInsertRowid) }; }
async delete(id: number): Promise<boolean> { return this.db.prepare("DELETE FROM jobs WHERE id = ?").run(id).changes > 0; }}
interface JobRow { id: number; filename: string; preset: string; state: Job["state"]; progress: number; created_at: string;}
function toJob(row: JobRow): Job { return { id: row.id, filename: row.filename, preset: row.preset, state: row.state, progress: row.progress, createdAt: row.created_at, };}Vier Handwerkspunkte daran:
CREATE TABLE IF NOT EXISTSim Konstruktor macht das Repository selbstinitialisierend: Erster Start legt das Schema an, jeder weitere findet es vor.- Platzhalter (
?) sind Pflicht. Werte werden nie in den SQL-Text hineinformatiert (`... WHERE filename = '${name}'`ist verboten): Erstens zerbricht es an einem Apostroph im Dateinamen, zweitens ist es das Einfallstor für SQL-Injection, bei der ein bösartiger Eingabewert zum SQL-Befehl wird. Prepared Statements mit Platzhaltern erledigen beides. - Die Zeile ist nicht das Objekt. Die Datenbank spricht
snake_caseund kennt keincreatedAt; die kleine AbbildungsfunktiontoJobübersetzt zwischen Tabellenzeile und Domänenobjekt. Diese Übersetzung gehört ins Repository und nirgendwohin sonst; oberhalb davon existiert nurJob. - Die Datenbankdatei (
data/render-farm.db) gehört wie jede erzeugte Datei in die.gitignore; ins Repository gehört das Schema (der Code, der es anlegt), nicht der Datenbestand.
Der Umstieg selbst ist die bekannte eine Zeile in instances.ts, und alles oberhalb läuft unverändert; führen Sie den Beweis wirklich vor: Anwendung starten, Jobs anlegen, Dev-Server neu starten, die Jobs sind noch da.
Fehler- und Randfälle
Abschnitt betitelt „Fehler- und Randfälle“Mit echter Persistenz werden die Randfälle verbindlich, und die Schichten sortieren, wer sie behandelt:
- Nicht gefunden: Das Repository liefert ehrlich
null; die Deutung (“404” oder “Fehler im Ablauf”) ist Sache von Service und Präsentation. - Duplikate: Fachliche Eindeutigkeit erzwingt die Datenbank per
UNIQUE-Constraint; der Verstoß kommt als Treiber-Fehler an und wird im Repository in einen fachlichen Fehler übersetzt (Konfliktfall, an der API409), nicht als roher SQL-Fehlertext durchgereicht. - Zusammengehörige Änderungen: Sollen mehrere Schreibvorgänge nur gemeinsam gelten (Job anlegen und Zähler erhöhen), fasst eine Transaktion sie zusammen: ganz oder gar nicht.
better-sqlite3bietet dafürdb.transaction(...); das Konzept kennt jede Datenbank, die Tiefe gehört INSY. Für dieses Jahr genügt: erkennen, wann eine Transaktion nötig ist.
Strukturierte Daten austauschen
Abschnitt betitelt „Strukturierte Daten austauschen“Der Lehrplan verlangt den Datenaustausch zwischen Anwendungen über strukturierte Formate; die Datenbank allein leistet das nicht, denn sie ist das private Innenleben einer Anwendung. Ausgetauscht wird über die Grenzformate, und beide sind vorhanden:
- Die API ist der Live-Austausch: Ein fremdes Programm liest
GET /api/jobsund bekommt den dokumentierten JSON-Vertrag, egal ob dahinter Datei oder SQLite liegt. Die Persistenzschicht ist durch die Architektur unsichtbar; genau deshalb durfte sie wechseln, ohne Konsumenten zu brechen. - Export und Import sind der Austausch als Datei: ein Endpunkt oder Kommando, das den Datenbestand als JSON-Dokument liefert (
GET /api/jobs/export), und ein Import, der ein solches Dokument einliest, validiert und übernimmt. Das Format ist wieder ein Vertrag mit Versionsfeld und Beispieldokument; der Import behandelt die drei bekannten Fehlerklassen jeder Datengrenze (fehlt, syntaktisch kaputt, inhaltlich falsch).
Die Nagelprobe dazu ist Übung 19: Zwei Projekte tauschen ihre Exportdateien und importieren sie gegenseitig. Alles, was dabei knirscht (undokumentierte Felder, andere Datumsformate, abweichende Zustandsnamen), ist eine Vertragslücke, gefunden zum billigsten möglichen Zeitpunkt.
Lernergebnisse: Was Sie nach diesem Kapitel können sollten
Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:
- Erklären: die Grenzen der Datei-Persistenz (gleichzeitige Zugriffe, Suchen, Teilschreiben) und den Einsatzbereich von SQLite erklären.
- Anwenden: SQLite mit
better-sqlite3in ein Next.js-Projekt einbinden, Schema selbstinitialisierend anlegen und ein Repository gegen das bestehende Interface implementieren. - Anwenden: ausschließlich Prepared Statements mit Platzhaltern verwenden und die SQL-Injection-Gefahr erklären.
- Anwenden: zwischen Tabellenzeilen und Domänenobjekten im Repository übersetzen.
- Erklären: Randfälle (nicht gefunden, Duplikate, zusammengehörige Änderungen) den zuständigen Schichten zuordnen und den Transaktionsgedanken erläutern.
- Entwerfen: Export- und Importformate als Verträge definieren und Daten zwischen zwei Anwendungen austauschen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 19 - Vom File zur Datenbank