Abschnitt 18 · Lektion 2
Die Datenbasis
Bis Abschnitt 14 hatte die Notizliste eine Tabelle, und alle Notizen darin gehörten allen. Das ging, solange es niemanden gab, dem sie hätten gehören können. Sobald sich jemand anmeldet, geht es nicht mehr: Eine Liste, die jeder sieht, ist keine persönliche Liste.
Zwei Tabellen, weil es zwei Dinge sind
<?php
declare(strict_types=1);
// Zwei Tabellen, und die zweite zeigt auf die erste. Genau das brauchst du
// fuer das Projekt: Lesezeichen gehoeren jemandem.
$db = new PDO("sqlite::memory:", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("CREATE TABLE benutzer (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
)");
$db->exec("CREATE TABLE lesezeichen (
id INTEGER PRIMARY KEY,
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
titel TEXT NOT NULL
)");
$db->exec("INSERT INTO benutzer (name) VALUES ('mia'), ('tom')");
$db->exec("INSERT INTO lesezeichen (benutzer_id, titel) VALUES
(1, 'Astro'), (1, 'PHP-Handbuch'), (2, 'Toms Werkstatt')");
// Die Spalten stehen ausgeschrieben da, nicht als *: Bei einem JOIN heissen
// zwei von ihnen id, und mit * ueberlebt nur die letzte.
$alle = $db->query("SELECT lesezeichen.titel AS titel, benutzer.name AS besitzer
FROM lesezeichen
JOIN benutzer ON benutzer.id = lesezeichen.benutzer_id
ORDER BY lesezeichen.id")->fetchAll();
foreach ($alle as $zeile) {
echo str_pad($zeile["titel"], 16), "gehoert ", $zeile["besitzer"], "\n";
}
// Und die Frage, die die Anwendung jedes Mal stellt: was gehoert mia?
$mias = $db->prepare("SELECT titel FROM lesezeichen WHERE benutzer_id = :b");
$mias->execute([":b" => 1]);
echo "\nMia hat ", count($mias->fetchAll()), " Lesezeichen.\n"; Die Faustregel ist erstaunlich stumpf: Für jede Art von Ding eine Tabelle. Benutzer sind eine Art, Lesezeichen sind eine andere. Sie in eine Tabelle zu quetschen, ginge nur mit leeren Spalten, und die wären genau so lange leer, bis jemand sie doch benutzt.
Zusammengehalten werden beide von einer einzigen Spalte: benutzer_id. Sie steht beim Lesezeichen
und enthält die id aus der Benutzertabelle. Man nennt das einen Fremdschlüssel, und das Wort
klingt größer als die Sache. Es ist eine Zahl, die woanders hinzeigt.
Sieh dir im ersten Beispiel die beiden Abfragen an. Die erste holt mit JOIN beide Seiten zusammen,
weil sie die Namen ausgeben will. Die zweite ist die, die deine Anwendung dauernd stellen wird: Gib
mir alles, was zu dieser einen benutzer_id gehört. Dafür braucht es kein JOIN, nur ein WHERE.
Und noch etwas steht dort, das aus Abschnitt 14 stammt: Die Spalten sind ausgeschrieben statt als
Stern. Bei einem JOIN heißen zwei von ihnen id, und mit SELECT * überlebt davon nur die letzte.
Was REFERENCES wirklich tut
<?php
declare(strict_types=1);
// REFERENCES steht in der Tabelle, aber SQLite haelt sich nur daran, wenn
// man es darum bittet. Und die Bitte gilt je Verbindung.
function baueDatenbank(bool $mitPruefung): PDO
{
$db = new PDO("sqlite::memory:", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
if ($mitPruefung) {
$db->exec("PRAGMA foreign_keys = ON");
}
$db->exec("CREATE TABLE benutzer (id INTEGER PRIMARY KEY, name TEXT NOT NULL)");
$db->exec("CREATE TABLE lesezeichen (
id INTEGER PRIMARY KEY,
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
titel TEXT NOT NULL
)");
$db->exec("INSERT INTO benutzer (id, name) VALUES (1, 'mia')");
return $db;
}
foreach ([false, true] as $mitPruefung) {
$db = baueDatenbank($mitPruefung);
echo $mitPruefung ? "Mit PRAGMA foreign_keys = ON: " : "So wie SQLite startet: ";
echo "PRAGMA sagt ", $db->query("PRAGMA foreign_keys")->fetchColumn(), ", ";
try {
// Benutzer 99 gibt es nicht. Trotzdem?
$db->exec("INSERT INTO lesezeichen (benutzer_id, titel) VALUES (99, 'Geisterzeichen')");
echo "das Geisterzeichen ist drin.\n";
} catch (PDOException $e) {
echo $e->getMessage(), "\n";
}
} REFERENCES benutzer(id) in der Spaltendefinition sieht aus wie ein Versprechen: Hier steht nur eine
Zahl, die es drüben wirklich gibt. Das zweite Beispiel zeigt, dass SQLite dieses Versprechen erst
einmal gar nicht hält. Ohne weiteres Zutun schreibt es ein Lesezeichen für Benutzer 99 anstandslos in
die Tabelle, obwohl es Benutzer 99 nicht gibt.
Der Schalter heißt PRAGMA foreign_keys = ON, und er gilt je Verbindung, nicht je Datenbank. Wer
ihn will, setzt ihn also gleich hinter dem Verbindungsaufbau, jedes Mal. Mit ihm meldet sich derselbe
Einfügeversuch als FOREIGN KEY constraint failed.
Trotzdem steht REFERENCES auch ohne den Schalter nicht umsonst da. Es ist die Stelle, an der ein
Mensch die Beziehung ablesen kann, und andere Datenbanken halten sich von sich aus daran. In diesem
Projekt bleibt es bei der Deklaration ohne Schalter: Der Code selbst gibt nie eine erfundene Kennung
weiter, weil sie immer aus der Sitzung kommt.
Die Tabellen legt die Klasse an
<?php
declare(strict_types=1);
// Die Speicherklasse legt ihre Tabellen bei jedem Start an. Das geht nur
// wegen der drei Woerter IF NOT EXISTS, und es geht nur so weit.
$datei = __DIR__ . "/probe.db";
@unlink($datei);
$anlegen = "CREATE TABLE IF NOT EXISTS lesezeichen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL
)";
foreach ([1, 2, 3] as $start) {
$db = new PDO("sqlite:" . $datei, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->exec($anlegen);
$db->exec("INSERT INTO lesezeichen (titel) VALUES ('Nummer $start')");
echo "Start ", $start, ": Tabelle da, Zeilen: ",
$db->query("SELECT count(*) FROM lesezeichen")->fetchColumn(), "\n";
}
// Und jetzt der Tag, an dem eine Spalte dazukommen soll. IF NOT EXISTS
// hilft dabei nicht: Die Tabelle gibt es ja, also passiert gar nichts.
$db = new PDO("sqlite:" . $datei, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->exec("CREATE TABLE IF NOT EXISTS lesezeichen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
url TEXT NOT NULL
)");
$spalten = array_column($db->query("PRAGMA table_info(lesezeichen)")->fetchAll(PDO::FETCH_ASSOC), "name");
echo "\nNach dem erweiterten CREATE: ", implode(", ", $spalten), "\n";
$db->exec("ALTER TABLE lesezeichen ADD COLUMN url TEXT NOT NULL DEFAULT ''");
$spalten = array_column($db->query("PRAGMA table_info(lesezeichen)")->fetchAll(PDO::FETCH_ASSOC), "name");
echo "Nach ALTER TABLE: ", implode(", ", $spalten), "\n";
@unlink($datei); In Lektion 14.2 hast du die Tabelle einmal von Hand angelegt. Auf einem Server, den du gerade aufgesetzt hast, kann das niemand für dich tun. Die übliche Antwort für ein kleines Projekt: Die Speicherklasse legt an, was sie braucht, und zwar bei jedem Start.
Möglich machen das die drei Wörter IF NOT EXISTS. Damit legt CREATE TABLE die Tabelle an, wenn
es sie noch nicht gibt, und tut sonst nichts. Das dritte Beispiel startet dieselbe Datei dreimal hintereinander,
und die Zeilen bleiben stehen.
Und es zeigt, wo diese Bequemlichkeit endet. Am Tag, an dem eine Spalte dazukommen soll, hilft
IF NOT EXISTS nicht mehr: Die Tabelle gibt es ja, also passiert gar nichts, und die neue Spalte
fehlt schweigend. Dafür gibt es ALTER TABLE, und ab einer gewissen Größe eine eigene Datei je
Änderung, durchnummeriert. Man nennt das Migrationen. Für dieses Projekt reicht der einfache Weg, und
es ist gut, seine Grenze zu kennen, bevor man an sie stößt.
Was in die Speicherklasse gehört und was nicht
Die Klasse, die du gleich schreibst, ist die einzige Stelle des Projekts, die SQL enthält. Das ist die ganze Regel, und sie hat zwei Richtungen.
Nach innen heißt sie: Jede Abfrage steht hier. Wer wissen will, was die Anwendung mit ihrer Datenbank tut, liest eine Datei und nicht zwölf.
Nach außen heißt sie: Hier steht nichts anderes. Kein echo, kein header(), keine Entscheidung
darüber, wer etwas sehen darf. Die Klasse beantwortet Fragen wie „gib mir alle Lesezeichen von
Benutzer 3” und stellt keine. Genau deshalb lässt sie sich in Abschnitt 17 mit drei Zeilen testen,
und genau deshalb kannst du sie später gegen eine andere austauschen.
Ein Nebeneffekt davon steht schon im Startcode: Die Klasse nimmt den Pfad zur Datenbankdatei entgegen, statt ihn zu kennen. Das ist die Regel aus Lektion 17.4, und in der letzten Lektion dieses Abschnitts zahlt sie sich aus.
Zum Mitnehmen
Eine Anwendung, in der Daten jemandem gehören, braucht zwei Tabellen und eine Spalte, die von der einen auf die andere zeigt. Das ist alles. Der Rest dieser Lektion ist die Frage, wer diese Tabellen anlegt.
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.