Kodokon kodokon.com

Persistenz mit SQLite: die Datenzugriffsschicht

Nutze better-sqlite3 mit Prepared Statements, Transaktionen und einem eigenen Repository für schnelle und sichere Persistenz.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

SQLite läuft innerhalb deines Prozesses: kein Server, kein Netzwerk, eine Latenz pro Abfrage in der Größenordnung einer Mikrosekunde. Genau das macht die synchrone API von better-sqlite3 nicht nur akzeptabel, sondern oft schneller als einen asynchronen Treiber: Es lohnt sich nicht, für einen Lesevorgang, der kürzer als ein Tick ist, ein Promise und eine Runde der Event Loop zu bezahlen. Der Kompromiss ist beim Schreiben real: Eine langsame Abfrage blockiert die Loop. Halte deine Abfragen indiziert und kurz, dann bewältigt SQLite zehntausende Lesevorgänge pro Sekunde.

BASH
npm install better-sqlite3
Natives Modul: Es wird bei der Installation bei Bedarf kompiliert.

Beim Öffnen der Datenbank machen zwei Einstellungen den Unterschied. Der WAL-Modus (write-ahead logging) erlaubt Lesevorgänge während eines Schreibvorgangs, statt die gesamte Datei zu sperren. Und das Schema wird idempotent mit CREATE TABLE IF NOT EXISTS angewendet - völlig ausreichend, bevor du ein echtes Migrationswerkzeug einführst.

JAVASCRIPT
import Database from 'better-sqlite3';

export function createDb(path = 'app.db') {
  const db = new Database(path);
  db.pragma('journal_mode = WAL');
  db.exec(`CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
  )`);
  return db;
}
connection.js: Öffnen, Einstellungen, idempotentes Schema.

Die Datenzugriffsschicht - das Repository - ist das einzige Modul, das SQL spricht. Die Abfragen werden einmal bei der Konstruktion vorbereitet und dann bei jedem Aufruf wiederverwendet: Die Engine parst den SQL-Text nicht erneut, und die Werte laufen über Platzhalter ?, niemals über Verkettung. Es ist genau die Schnittstelle findAll, findByEmail, insert, die der Service aus der vorherigen Lektion erwartet.

JAVASCRIPT
export function createUserRepository(db) {
  const insertStmt = db.prepare(
    'INSERT INTO users (email, name) VALUES (?, ?)',
  );
  const byEmailStmt = db.prepare(
    'SELECT * FROM users WHERE email = ?',
  );
  const allStmt = db.prepare(
    'SELECT * FROM users ORDER BY id',
  );
  return {
    insert(user) {
      const info = insertStmt.run(user.email, user.name);
      return { id: info.lastInsertRowid, ...user };
    },
    findByEmail(email) {
      return byEmailStmt.get(email);
    },
    findAll() {
      return allStmt.all();
    },
  };
}
Einmal vorbereitet, tausendmal ausgeführt: run, get, all.
JAVASCRIPT
import { createDb } from './connection.js';

const db = createDb();
const insertStmt = db.prepare(
  'INSERT INTO users (email, name) VALUES (?, ?)',
);
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertStmt.run(user.email, user.name);
  }
});
insertMany([
  { email: 'ada@example.com', name: 'Ada' },
  { email: 'linus@example.com', name: 'Linus' },
]);
db.transaction: Alles wird übernommen, oder alles wird zurückgerollt.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum ist die synchrone API von better-sqlite3 in Node kein Widerspruch?
    • Weil Node native Module in einem separaten Thread ausführt
    • Weil SQLite im Prozess läuft: Die Abfrage kostet weniger als die asynchrone Orchestrierung, die sie sonst vermeiden würde
    • Weil Schreibvorgänge automatisch in eine Warteschlange gestellt werden
    • Weil der WAL-Modus jede Abfrage nicht-blockierend macht
  2. Was garantieren die Platzhalter ? in einem Prepared Statement?
    • Die Werte werden getrennt vom SQL übergeben und können niemals als Code ausgeführt werden
    • Die Werte werden vor dem Speichern verschlüsselt
    • Die Abfrage wird automatisch auf der Platte zwischengespeichert
  3. Welchen doppelten Vorteil bringt db.transaction bei tausend Inserts?
    • Datenkompression und automatische Deduplizierung
    • Atomarität (alles oder nichts) und eine einzige Synchronisierung mit der Platte statt tausend
    • Schema-Validierung und Erstellung fehlender Indizes