Zum Inhalt

Datenbank (SQL)

Mit db stellt ein Skript Fragen an die Datenbank („Wie viele …?", „Gib mir alle …") und schreibt Daten hinein. Die Sprache dafür heißt SQL. Du brauchst nur wenige Wörter davon: SELECT (lesen), INSERT (einfügen), UPDATE (ändern) und DELETE (löschen).

Ausführliches dazu: db — Datenbankzugriff.

Werte immer als Parameter übergeben

Werte aus dem Formular gehören nie direkt in den SQL-Text, sondern als Parameter (? oder ${name}). Sonst kann jemand über ein Eingabefeld fremde SQL-Befehle einschleusen („SQL-Injection"). Alle Beispiele hier machen es richtig.

Vorbereitung: Übungstabellen anlegen

Damit du gefahrlos üben kannst, legt dieses Skript drei kleine Tabellen an und füllt sie mit Beispieldaten. Führe es einmal in der Skript-Konsole aus. Das letzte Beispiel der Seite räumt die Tabellen wieder weg.

db.update("CREATE TABLE IF NOT EXISTS bsp_kunde ("
  + " id BIGINT AUTO_INCREMENT PRIMARY KEY,"
  + " kundennr VARCHAR(20) NOT NULL,"
  + " name VARCHAR(100),"
  + " ort VARCHAR(60),"
  + " umsatz DECIMAL(12,2) DEFAULT 0,"
  + " kreditlimit DECIMAL(12,2),"
  + " email VARCHAR(120))");
db.update("CREATE TABLE IF NOT EXISTS bsp_bestellung ("
  + " id BIGINT AUTO_INCREMENT PRIMARY KEY,"
  + " kundennr VARCHAR(20) NOT NULL,"
  + " datum DATE,"
  + " betrag DECIMAL(12,2),"
  + " status VARCHAR(20))");
db.update("CREATE TABLE IF NOT EXISTS bsp_konto ("
  + " id BIGINT PRIMARY KEY,"
  + " inhaber VARCHAR(100),"
  + " saldo DECIMAL(12,2))");

if (db.scalar("SELECT COUNT(*) FROM bsp_kunde") == 0) {
  db.batch("INSERT INTO bsp_kunde (kundennr, name, ort, umsatz, kreditlimit, email) VALUES (?, ?, ?, ?, ?, ?)", [
    ["K-100", "Bäckerei Schmitz",   "Köln",   12500.00, 5000,  "info@schmitz.example"],
    ["K-101", "Autohaus Weber",     "Berlin", 98000.50, 20000, "kontakt@weber.example"],
    ["K-102", "Blumen Meyer",       "Köln",    3400.00, 1000,  null],
    ["K-103", "Druckerei Hoffmann", "Hagen",  45000.00, 10000, "druck@hoffmann.example"],
    ["K-104", "Café Sommer",        "Berlin",  8900.00, 2000,  "hallo@sommer.example"],
    ["K-105", "Elektro Wagner",     "Hagen",  27000.00, 8000,  "service@wagner.example"]
  ]);
  db.batch("INSERT INTO bsp_bestellung (kundennr, datum, betrag, status) VALUES (?, ?, ?, ?)", [
    ["K-100", "2026-01-15", 250.00, "bezahlt"],
    ["K-100", "2026-03-02", 410.50, "offen"],
    ["K-101", "2026-02-20", 15000.00, "bezahlt"],
    ["K-103", "2026-03-10", 3200.00, "offen"],
    ["K-104", "2026-03-11", 89.90, "storniert"]
  ]);
  db.batch("INSERT INTO bsp_konto (id, inhaber, saldo) VALUES (?, ?, ?)", [
    [1, "Kasse", 1000.00],
    [2, "Bank",  5000.00]
  ]);
}
log.info("Übungstabellen bereit");

Die Tabellen sind für MySQL/MariaDB geschrieben. Auf PostgreSQL heißt BIGINT AUTO_INCREMENT dort BIGSERIAL.

Einsteiger

1. Wie viele Benutzer gibt es?

db.scalar liefert genau einen Wert — ideal zum Zählen.

var anzahl = db.scalar("SELECT COUNT(*) FROM USERS");
log.info("Es gibt " + anzahl + " Benutzer.");

Auf MySQL unter Linux zählt die Groß-/Kleinschreibung bei Tabellennamen: Die Benutzertabelle heißt USERS.

2. Alle Kunden ausgeben

db.query liefert eine Liste von Zeilen. Jede Zeile hat die Spaltennamen als Eigenschaften — immer kleingeschrieben.

var kunden = db.query("SELECT kundennr, name, ort FROM bsp_kunde");
for (var i = 0; i < kunden.length; i++) {
  log.info(kunden[i].kundennr + ": " + kunden[i].name + " (" + kunden[i].ort + ")");
}

3. Einen Kunden über seine Nummer finden

Das ? ist ein Platzhalter. Der Wert dahinter wird sicher eingesetzt. db.queryOne liefert die erste Zeile oder null.

var kunde = db.queryOne("SELECT name, ort FROM bsp_kunde WHERE kundennr = ?", "K-101");
if (kunde) {
  log.info(kunde.name + " aus " + kunde.ort);
} else {
  log.info("Kunde nicht gefunden");
}

4. Prüfen, ob es etwas gibt

db.exists antwortet mit true oder false.

if (db.exists("SELECT 1 FROM bsp_kunde WHERE ort = ?", "Hagen")) {
  log.info("Kunden in Hagen gibt es.");
}

5. Eine Summe bilden

SUM addiert eine Spalte. Geldbeträge kommen als genaue Dezimalzahl zurück — kc.num macht eine normale Zahl zum Rechnen daraus, kc.numFormat einen fertigen Anzeigetext.

var summe = kc.num(db.scalar("SELECT SUM(umsatz) FROM bsp_kunde"));
log.info("Gesamtumsatz: " + kc.numFormat(summe, null, "€", 0, 2, ",", ".", 3));

An einer Datenbankzeile gibt es die Kurzform .numFormat(…) nicht — die hängt nur an deklarierten Feldern. kc.numFormat(wert, …) kann dasselbe und ist hier die einzige Möglichkeit.

6. Filtern und sortieren

WHERE filtert, ORDER BY sortiert (DESC = absteigend).

var zeilen = db.query(
  "SELECT name, umsatz FROM bsp_kunde WHERE umsatz > ? ORDER BY umsatz DESC", 10000);
zeilen.forEach(function (z) {
  log.info(z.name + ": " + z.umsatz);
});

7. Mit Platzhalter suchen (LIKE)

% steht für „beliebige Zeichen". Es gehört in den Wert, nicht in den SQL-Text.

var begriff = "%" + ctx.form.suchbegriff + "%";
var treffer = db.query("SELECT name FROM bsp_kunde WHERE LOWER(name) LIKE LOWER(?)", begriff);
log.info(treffer.length + " Treffer");
treffer.forEach(function (t) { log.info("- " + t.name); });

8. Einen Datensatz einfügen

db.update führt INSERT, UPDATE und DELETE aus und meldet, wie viele Zeilen betroffen waren.

var n = db.update(
  "INSERT INTO bsp_kunde (kundennr, name, ort) VALUES (?, ?, ?)",
  "K-900", "Testkunde", "Bonn");
log.info(n + " Zeile(n) eingefügt");

9. Einen Wert ändern

var n = db.update("UPDATE bsp_kunde SET ort = ? WHERE kundennr = ?", "Siegburg", "K-900");
log.info("geändert: " + n);

Kommt 0 zurück, gab es keinen passenden Datensatz.

10. Einen Datensatz löschen

var n = db.update("DELETE FROM bsp_kunde WHERE kundennr = ?", "K-900");
log.info("gelöscht: " + n);

Fortgeschritten

11. Benannte Parameter

Bei mehreren Werten ist ${name} übersichtlicher als viele ?. Die Werte stehen dann in einem Objekt.

var zeilen = db.query(
  "SELECT name FROM bsp_kunde WHERE ort = ${ort} AND umsatz >= ${mindestens}",
  { ort: "Köln", mindestens: 5000 });
log.info("Treffer: " + zeilen.length);

12. Werte aus der Datenbank ins Formular holen

Typisch im onLoad-Hook oder wenn der Benutzer eine Nummer eingibt.

var k = db.queryOne(
  "SELECT name, ort, email FROM bsp_kunde WHERE kundennr = ${nr}",
  { nr: ctx.form.kundennr });
if (k) {
  ctx.form.name  = k.name;
  ctx.form.land  = k.ort;
  ctx.form.email = k.email;
  log.info("Übernommen: " + k.name);
} else {
  ctx.addError("kundennr", "Unbekannte Kundennummer.");
}

13. Einfügen und die neue ID erhalten

db.insert liefert die Nummer, die die Datenbank vergeben hat.

var id = db.insert(
  "INSERT INTO bsp_bestellung (kundennr, datum, betrag, status) VALUES (${k}, ${d}, ${b}, 'offen')",
  { k: "K-102", d: new Date().format("yyyy-MM-dd"), b: 75.20 });
log.info("Neue Bestellung Nr. " + id);

14. Gruppieren und zählen

GROUP BY fasst Zeilen zusammen. Pro Ort erhältst du Anzahl und Summe.

var orte = db.query(
  "SELECT ort, COUNT(*) AS anzahl, SUM(umsatz) AS summe"
  + " FROM bsp_kunde GROUP BY ort ORDER BY ort");
orte.forEach(function (o) {
  log.info(o.ort + ": " + o.anzahl + " Kunden, "
         + kc.numFormat(o.summe, null, "€", 0, 2, ",", ".", 3));
});

Berechnete Spalten bekommen mit AS einen Namen, damit du sie lesen kannst.

15. Zwei Tabellen verbinden (JOIN)

Zu jeder Bestellung den Kundennamen dazuholen.

var zeilen = db.query(
  "SELECT b.id, b.betrag, b.status, k.name"
  + " FROM bsp_bestellung b JOIN bsp_kunde k ON k.kundennr = b.kundennr"
  + " ORDER BY b.id");
zeilen.forEach(function (z) {
  log.info("#" + z.id + " " + z.name + ": " + z.betrag + " € (" + z.status + ")");
});

16. Nur die ersten Treffer

db.limit hängt die passende Begrenzung für jede Datenbank an.

var sql = db.limit("SELECT name, umsatz FROM bsp_kunde ORDER BY umsatz DESC", 3);
var top = db.query(sql);
log.info("Top 3: " + top.map(function (t) { return t.name; }).join(", "));

17. Viele Zeilen auf einmal einfügen

db.batch ist bei vielen Zeilen deutlich schneller als eine Schleife mit einzelnen db.update.

var neue = [
  { k: "K-104", b: 12.50 },
  { k: "K-104", b: 33.00 },
  { k: "K-105", b: 480.00 }
];
var ergebnis = db.batch(
  "INSERT INTO bsp_bestellung (kundennr, datum, betrag, status) VALUES (${k}, CURRENT_DATE, ${b}, 'offen')",
  neue);
log.info(ergebnis.length + " Bestellungen eingefügt");

18. Große Mengen Zeile für Zeile

db.forEach holt nicht alles auf einmal in den Speicher. Die Funktion wird für jede Zeile aufgerufen.

var anzahl = 0, summe = 0;
db.forEach("SELECT betrag FROM bsp_bestellung WHERE status = ?", function (z) {
  anzahl++;
  summe += kc.num(z.betrag);
}, "offen");
log.info("Offene Bestellungen: " + anzahl + ", zusammen "
       + kc.numFormat(summe, null, "€", 0, 2, ",", ".", 3));

Die Parameter stehen hier hinter der Funktion.

19. Eine ganze Zeile ausgeben

JSON.stringify(zeile) zeigt bei Datenbankzeilen nur {}. Diese kleine Funktion geht alle Spalten durch.

function zeileAlsText(zeile) {
  var teile = [];
  for (var spalte in zeile) teile.push(spalte + "=" + zeile[spalte]);
  return teile.join(", ");
}

var z = db.queryOne("SELECT * FROM bsp_kunde WHERE kundennr = ?", "K-100");
log.info(zeileAlsText(z));

20. Mit leeren Werten (NULL) umgehen

Fehlt ein Wert, liefert die Datenbank null. COALESCE setzt einen Ersatzwert ein — oder du prüfst im Skript.

var zeilen = db.query(
  "SELECT name, COALESCE(email, 'keine E-Mail') AS email FROM bsp_kunde WHERE ort = ?", "Köln");
zeilen.forEach(function (z) { log.info(z.name + ": " + z.email); });

var ohne = db.scalar("SELECT COUNT(*) FROM bsp_kunde WHERE email IS NULL");
log.info(ohne + " Kunde(n) ohne E-Mail");

Für NULL gibt es in SQL IS NULL — = NULL findet nie etwas.

21. Nach Datum filtern

Das Datum rechnest du im Skript aus und übergibst es als Text im Format JJJJ-MM-TT. So funktioniert es mit jeder Datenbank.

var ab = new Date();
ab.setDate(ab.getDate() - 400);   // die letzten 400 Tage
var abText = ab.format("yyyy-MM-dd");

var n = db.scalar("SELECT COUNT(*) FROM bsp_bestellung WHERE datum >= ?", abText);
log.info(n + " Bestellungen seit " + ab.format("dd.MM.yyyy"));

Profi

22. Transaktion: alles oder nichts

db.tx klammert mehrere Schritte. Läuft alles durch, wird gespeichert. Tritt ein Fehler auf, wird alles zurückgenommen.

var nr = "K-105", betrag = 120.00;

var id = db.tx(function (t) {
  var neueId = t.insert(
    "INSERT INTO bsp_bestellung (kundennr, datum, betrag, status) VALUES (${k}, CURRENT_DATE, ${b}, 'offen')",
    { k: nr, b: betrag });
  t.update("UPDATE bsp_kunde SET umsatz = umsatz + ${b} WHERE kundennr = ${k}", { k: nr, b: betrag });
  return neueId;   // wird zum Rückgabewert von db.tx
});
log.info("Bestellung gebucht: #" + id);

Innerhalb von db.tx immer das übergebene t benutzen, nicht db.

23. Umbuchen mit Prüfung und Rücknahme

Geld von einem Konto auf ein anderes. Reicht das Guthaben nicht, bricht throw die Transaktion ab — und die erste Buchung wird automatisch rückgängig gemacht.

function umbuchen(von, nach, betrag) {
  db.tx(function (t) {
    t.update("UPDATE bsp_konto SET saldo = saldo - ? WHERE id = ?", betrag, von);
    var neu = kc.num(t.scalar("SELECT saldo FROM bsp_konto WHERE id = ?", von));
    if (neu < 0) {
      throw new Error("Nicht genug Guthaben (" + (neu + betrag) + " €)");
    }
    t.update("UPDATE bsp_konto SET saldo = saldo + ? WHERE id = ?", betrag, nach);
  });
}

try {
  umbuchen(1, 2, 5000);            // Kasse hat nur 1000 €
} catch (e) {
  log.warn("Abgelehnt: " + e.message);
}
var kasse = kc.num(db.scalar("SELECT saldo FROM bsp_konto WHERE id = 1"));
log.info("Kasse: " + kasse);     // unverändert 1000

24. Anlegen oder aktualisieren („Upsert")

Gibt es den Datensatz schon, wird er geändert, sonst neu angelegt. Diese Form funktioniert mit jeder Datenbank.

function speichereKunde(k) {
  var geaendert = db.update(
    "UPDATE bsp_kunde SET name = ${name}, ort = ${ort} WHERE kundennr = ${nr}", k);
  if (geaendert === 0) {
    db.update("INSERT INTO bsp_kunde (kundennr, name, ort) VALUES (${nr}, ${name}, ${ort})", k);
    return "angelegt";
  }
  return "aktualisiert";
}

log.info(speichereKunde({ nr: "K-200", name: "Neu GmbH", ort: "Essen" }));   // angelegt
log.info(speichereKunde({ nr: "K-200", name: "Neu GmbH & Co. KG", ort: "Essen" }));   // aktualisiert
db.update("DELETE FROM bsp_kunde WHERE kundennr = ?", "K-200");   // Übung aufräumen

25. Doppelte Einträge finden

HAVING filtert nach dem Gruppieren — hier: Orte mit mehr als einem Kunden.

var doppelt = db.query(
  "SELECT ort, COUNT(*) AS anzahl FROM bsp_kunde GROUP BY ort HAVING COUNT(*) > 1");
doppelt.forEach(function (d) {
  log.info(d.ort + " kommt " + d.anzahl + "-mal vor (mehrfach)");
});

26. Die Datenbank erkunden

Welche Datenbank ist das, welche Tabellen und Spalten gibt es?

var m = db.meta();
log.info("Datenbank: " + m.product + " " + m.version + " (" + m.dialect + ")");

var tabellen = db.tables().filter(function (t) { return t.indexOf("bsp_") === 0; });
log.info("Übungstabellen: " + tabellen.join(", "));

db.columns("bsp_kunde").forEach(function (c) {
  log.info("Spalte " + c.name + ": " + c.type + (c.nullable ? "" : " (Pflicht)"));
});

27. Eine andere Datenbank ansprechen

Externe Datenbanken richtet der Admin unter Datenquellen ein. Im Skript wählst du sie mit db.on("name") — danach gelten dieselben Befehle.

var extern = db.on("demo-mysql");       // Name der Datenquelle
if (extern.ping()) {
  log.info("Datenquelle erreichbar: " + extern.source + " (" + extern.dialect + ")");
  log.info("Kunden dort: " + extern.scalar("SELECT COUNT(*) FROM bsp_kunde"));
} else {
  log.warn("Datenquelle nicht erreichbar");
}

In der Beispielumgebung zeigt demo-mysql auf dieselbe Datenbank.

28. Unterschiedliches SQL je Datenbank

Manche Befehle heißen je Datenbank anders. db.dialect sagt, mit welcher du sprichst.

var sql;
switch (db.dialect) {
  case "oracle":    sql = "SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD') AS heute FROM dual"; break;
  case "sqlserver": sql = "SELECT CONVERT(varchar(10), GETDATE(), 23) AS heute"; break;
  default:          sql = "SELECT CURRENT_DATE AS heute";   // MySQL, PostgreSQL
}
log.info("Heute laut Datenbank: " + db.queryOne(sql).heute);

29. Eine Eingabe gegen die Datenbank prüfen

Im onValidate-Hook: Gibt es den Kunden, und passt der Betrag noch in sein Kreditlimit (einschließlich offener Bestellungen)?

var k = db.queryOne(
  "SELECT kreditlimit FROM bsp_kunde WHERE kundennr = ${nr}", { nr: ctx.form.kundennr });

if (!k) {
  ctx.addError("kundennr", "Diese Kundennummer gibt es nicht.");
} else {
  var offen = kc.num(db.scalar(
    "SELECT COALESCE(SUM(betrag), 0) FROM bsp_bestellung WHERE kundennr = ${nr} AND status = 'offen'",
    { nr: ctx.form.kundennr }));
  var frei = kc.num(k.kreditlimit) - offen;
  if ($betrag.num > frei) {
    ctx.addError("betrag", "Übersteigt das freie Kreditlimit von "
                        + kc.numFormat(frei, null, "€", 0, 2, ",", ".", 3) + ".");
  }
}

30. Aufräumen: Übungstabellen entfernen

Wenn du fertig bist, löscht dieses Skript die Übungstabellen wieder.

["bsp_bestellung", "bsp_kunde", "bsp_konto"].forEach(function (t) {
  db.update("DROP TABLE IF EXISTS " + t);
});
log.info("Übungstabellen entfernt");

Tabellennamen lassen sich nicht als Parameter übergeben — deshalb steht hier der Name im SQL-Text. Das ist nur in Ordnung, weil die Namen fest im Skript stehen und nicht aus einer Eingabe kommen.