Abschnitt 14 · Lektion 2
Verbinden und anlegen
PHP redet mit einer Datenbank nicht direkt, sondern über eine Zwischenschicht namens PDO, kurz für PHP Data Objects. Der Gewinn ist eine einzige Sache, und sie ist es wert: Der Code, mit dem du abfragst, einfügst und änderst, sieht bei SQLite genauso aus wie bei MySQL oder PostgreSQL. Unterschiedlich ist nur die allererste Zeile.
Der DSN sagt, wohin
Diese erste Zeile beschreibt das Ziel und heißt DSN, Data Source Name. Bei SQLite besteht sie aus
dem Wort sqlite, einem Doppelpunkt und einem Dateipfad, also sqlite:/pfad/zu/daten.db. Mehr
nicht: kein Rechnername, kein Benutzer, kein Passwort. Bei MySQL stünde dort
mysql:host=localhost;dbname=notizen, und dann kämen Benutzername und Passwort als zweites und
drittes Argument dazu.
Nimm den Pfad absolut, also mit __DIR__ davor. Ein relativer Pfad zeigt auf das
Arbeitsverzeichnis des Prozesses, und das ist nicht immer der Ordner, in dem deine Datei liegt. Bei
einem Webserver kann es ein ganz anderer sein, und dann legt PHP dir eine zweite, leere Datenbank
an, statt sich zu beschweren. Genau diese Sorte Fehler kostet einen Abend.
<?php
// Sieh dir an, wann die Datei auftaucht und wann sie waechst.
$datei = __DIR__ . "/daten.db";
echo "Vor der Verbindung da: ", file_exists($datei) ? "ja" : "nein", "\n";
$db = new PDO("sqlite:" . $datei, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
clearstatcache();
echo "Nach der Verbindung da: ", file_exists($datei) ? "ja" : "nein";
echo ", ", filesize($datei), " Bytes\n";
$db->exec("CREATE TABLE IF NOT EXISTS notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0
)");
clearstatcache();
echo "Nach der Tabelle: ", filesize($datei), " Bytes\n";
$db->exec("INSERT INTO notizen (titel) VALUES ('Milch')");
clearstatcache();
echo "Nach der ersten Notiz: ", filesize($datei), " Bytes\n"; Vor der Verbindung gibt es nichts, danach eine Datei mit 0 Bytes. Erst mit der ersten Tabelle wird sie 8192 Bytes groß, und die erste Notiz passt noch in dieselbe Seite hinein. Du musst also nichts vorbereiten und nichts anlegen: Verbinden genügt.
Im Terminal kannst du die Datei danach ansehen. ls -la zeigt sie mit ihren 8192 Bytes, und
head -c 16 daten.db gibt die ersten sechzehn Bytes aus: SQLite format 3, so fängt jede
SQLite-Datei an. Lass das Beispiel dann ein zweites Mal laufen, mit php index.php: Jetzt steht in
der ersten Zeile ja, die Datei ist ja noch da, und sie bleibt bei 8192 Bytes, obwohl eine zweite
Notiz dazukommt. Wer wieder von vorn anfangen will, löscht sie mit rm daten.db.
Die zwei Optionen, die der Kurs immer setzt
Das vierte Argument von new PDO ist ein Array mit Optionen. Zwei davon stehen ab jetzt in jeder
Verbindung dieses Kurses.
<?php
// Eine Notiz, zwei Abholarten. Sieh dir die Schluessel an.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$db->exec("CREATE TABLE IF NOT EXISTS notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0
)");
$db->exec("INSERT INTO notizen (titel) VALUES ('Milch')");
echo "Ohne die Option, also FETCH_BOTH:\n";
print_r($db->query("SELECT * FROM notizen")->fetch());
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
echo "\nMit PDO::FETCH_ASSOC:\n";
print_r($db->query("SELECT * FROM notizen")->fetch()); PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ist die wichtigere von beiden, weil ihre Vorgabe
wirklich stört. Ohne sie liefert PDO jede Zeile doppelt: einmal unter dem Spaltennamen und
einmal unter einer Nummer. Das siehst du im Beispiel, sechs Einträge für drei Spalten. Nichts daran
ist kaputt, es ist nur die doppelte Menge Daten für nichts, und ein foreach über eine solche Zeile
läuft zweimal durch dieselben Werte.
<?php
// Dieselbe Abfrage auf eine Tabelle, die es nicht gibt. Zweimal.
// Der einzige Unterschied ist eine Option.
$dsn = "sqlite:" . __DIR__ . "/daten.db";
$laut = new PDO($dsn, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
try {
$laut->query("SELECT * FROM gibtesnicht");
echo "Laut: hier kommt niemand an\n";
} catch (PDOException $e) {
echo "Laut: ", $e->getMessage(), "\n";
}
$still = new PDO($dsn, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT]);
$ergebnis = $still->query("SELECT * FROM gibtesnicht");
echo "Still: query() liefert ";
var_dump($ergebnis);
echo "Still: und der Grund steht nur hier: ", implode(" ", $still->errorInfo()), "\n";
echo "\nUnd das ist die Vorgabe, wenn du gar nichts angibst:\n";
$vorgabe = new PDO($dsn);
echo " ATTR_ERRMODE: ", $vorgabe->getAttribute(PDO::ATTR_ERRMODE);
echo " (SILENT=", PDO::ERRMODE_SILENT, ", WARNING=", PDO::ERRMODE_WARNING;
echo ", EXCEPTION=", PDO::ERRMODE_EXCEPTION, ")\n"; PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION sagt, was bei einem Fehler passieren soll. Im lauten
Modus fliegt eine PDOException, und die kennst du seit Abschnitt 7: Du fängst sie oder das
Programm bricht ab. Im stillen Modus gibt query() schlicht false zurück, und der Grund liegt in
errorInfo() und wartet darauf, dass jemand nachfragt. Tut das niemand, arbeitet dein Programm mit
false weiter, und der Fehler taucht drei Funktionen später als etwas ganz anderes auf.
Seit PHP 8.0 ist der laute Modus die Vorgabe. Das steht in der letzten Zeile des Beispiels:
ATTR_ERRMODE ist 2, und 2 ist EXCEPTION. Du brauchst die Option streng genommen also nicht mehr.
Sie steht hier trotzdem, und zwar aus zwei Gründen. Erstens siehst du sie in jedem Codeschnipsel im
Netz, weil sie vor PHP 8 wirklich nötig war, und du sollst wissen, was sie tut, statt sie
mitzukopieren. Zweitens macht sie sichtbar, worauf sich dein Code verlässt. Das ist billig und
erspart Rätselraten.
Eine dritte Option siehst du oft dazu, nämlich PDO::ATTR_EMULATE_PREPARES => false. Sie gehört zu
MySQL, wo PDO vorbereitete Abfragen von sich aus nachbaut, statt sie der Datenbank zu geben. Bei
SQLite gibt es nichts nachzubauen, der Treiber kennt die Option nicht einmal. Deshalb steht sie in
diesem Kurs nirgends. Wenn du später auf MySQL umsteigst, ist sie die eine Zeile, die du ergänzt.
Die Tabelle anlegen
<?php
// Erst dreimal dieselbe Tabelle, dann ein Blick auf die Spaltentypen.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("CREATE TABLE IF NOT EXISTS notizen (id INTEGER PRIMARY KEY, titel TEXT NOT NULL)");
echo "Erster Aufruf mit IF NOT EXISTS: durchgelaufen\n";
$db->exec("CREATE TABLE IF NOT EXISTS notizen (id INTEGER PRIMARY KEY, titel TEXT NOT NULL)");
echo "Zweiter Aufruf mit IF NOT EXISTS: durchgelaufen\n";
try {
$db->exec("CREATE TABLE notizen (id INTEGER PRIMARY KEY, titel TEXT NOT NULL)");
echo "Ohne IF NOT EXISTS: durchgelaufen\n";
} catch (PDOException $e) {
echo "Ohne IF NOT EXISTS: ", $e->getMessage(), "\n";
}
echo "\nSQLite kennt vier Sorten Werte. Hier steht ueberall dieselbe\n";
echo "Zeichenkette '42' drin, und die Spalte entscheidet, was daraus wird:\n\n";
$db->exec("DROP TABLE IF EXISTS probe");
$db->exec("CREATE TABLE probe (ganz INTEGER, text TEXT, komma REAL, egal BLOB)");
$db->exec("INSERT INTO probe (ganz, text, komma, egal) VALUES ('42', '42', '42', '42')");
$sorten = $db->query("SELECT typeof(ganz) AS ganz, typeof(text) AS text,
typeof(komma) AS komma, typeof(egal) AS egal FROM probe")->fetch();
foreach ($sorten as $spalte => $sorte) {
echo " ", str_pad($spalte, 10), $sorte, "\n";
} CREATE TABLE notizen (...) legt eine Tabelle an und beschwert sich, wenn es sie schon gibt. Beim
zweiten Aufruf deines Skripts wäre das ein Abbruch, und deshalb steht dort fast immer
CREATE TABLE IF NOT EXISTS. Damit darf das Skript beliebig oft laufen: Beim ersten Mal entsteht die
Tabelle, danach passiert nichts. Sieh dir dazu die Zeile mit DROP TABLE IF EXISTS probe weiter
unten im Beispiel an: Das CREATE TABLE probe darunter hat kein IF NOT EXISTS, und ohne das
Wegräumen käme ein zweites php index.php genau dort nicht mehr durch.
Sei dir dabei über eine Sache im Klaren: IF NOT EXISTS prüft nur den Namen. Änderst du später
eine Spalte, merkt SQLite davon nichts, weil die Tabelle ja da ist. Für eine geänderte Tabelle
braucht es ALTER TABLE, und das ist ein Thema für später.
Die Spaltentypen sind bei SQLite entspannter, als du es vielleicht erwartest. Es gibt nur vier
Sorten Werte: INTEGER für ganze Zahlen, REAL für Kommazahlen, TEXT für Zeichenketten und
BLOB für alles andere. Was du in die Klammer schreibst, ist eine Absichtserklärung: SQLite
schaut sich das Wort an und macht daraus die passende Sorte. Deshalb wird aus der Zeichenkette '42'
in einer INTEGER-Spalte die Zahl 42, in einer TEXT-Spalte der Text 42, und in einer
BLOB-Spalte bleibt sie, wie sie ist.
Drei Zusätze brauchst du in diesem Abschnitt wirklich:
INTEGER PRIMARY KEY macht die Spalte zum Primärschlüssel, und bei SQLite bekommt sie damit
noch etwas dazu: Wenn du beim Einfügen keine Nummer angibst, vergibt die Tabelle selbst die nächste
freie. Du schreibst also INSERT INTO notizen (titel) VALUES ('Milch') und bekommst deine id
geschenkt.
NOT NULL verbietet leere Werte in dieser Spalte. Ein Versuch endet als Ausnahme, und das ist
gut: Eine Notiz ohne Titel ist keine Notiz, und der beste Ort, das durchzusetzen, ist die Stelle,
an der die Daten wirklich landen.
DEFAULT 0 setzt einen Wert für den Fall, dass beim Einfügen keiner mitkommt. Bei erledigt ist
das genau richtig, denn eine neue Notiz ist immer offen.
Wahrheitswerte gibt es nicht
Dir ist vielleicht aufgefallen, dass erledigt eine Zahl ist und kein Wahrheitswert. Das liegt
nicht an mir: SQLite hat keinen Typ für wahr und falsch. Üblich ist deshalb INTEGER mit 0 für
nein und 1 für ja, und PHP zählt beim Auslesen genauso: Aus der Datenbank kommt eine Zahl zurück und
kein true.
Denk beim Vergleichen daran. if ($notiz["erledigt"]) funktioniert, weil 0 unwahr ist und 1 wahr,
aber if ($notiz["erledigt"] === true) ist nie erfüllt. In der Abfrage selbst schreibst du
WHERE erledigt = 0 und nicht WHERE erledigt = false.
Zum Mitnehmen
Zwei Zeilen Code, und du hast eine Datenbank. Es gibt keine Installation, keinen Dienst und kein Passwort: Die Datei entsteht in dem Moment, in dem du sie zum ersten Mal öffnest.
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 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.