mitmario.dev

Verbinden und anlegen

PHP Sandbox 5 Min Lesezeit 4 BeispieleLektion 2 von 9

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.

Die Datei entsteht beim ersten Zugriff
<?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.

Dieselbe Zeile, zweimal abgeholt
<?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.

Laut oder still, dieselbe kaputte Abfrage
<?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

Zweimal dasselbe CREATE
<?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, kostenlos

Zu 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.