mitmario.dev

Ändern und löschen

PHP Sandbox 4 Min Lesezeit 4 BeispieleLektion 5 von 9

INSERT, UPDATE und DELETE sind die drei Befehle, mit denen sich in einer Tabelle etwas ändert. Sie funktionieren alle gleich: Du bereitest sie mit prepare() vor, gibst die Werte an execute() mit, und danach fragst du nach, was passiert ist. Die Regel aus der letzten Lektion gilt hier genauso und ohne Ausnahme.

Die drei sehen so aus: INSERT INTO notizen (titel) VALUES (:titel) legt an, UPDATE notizen SET erledigt = 1 WHERE id = :id ändert, und DELETE FROM notizen WHERE id = :id entfernt.

Was du nach dem Ausführen wissen willst, ist bei den drei verschieden. Beim INSERT willst du die Nummer der neuen Zeile. Bei UPDATE und DELETE willst du wissen, ob überhaupt etwas getroffen wurde.

Die Nummer der neuen Zeile

Einfügen, und die Nummer, die dabei herauskommt
<?php

// Einfuegen, und die Nummer, die dabei herauskommt.

require __DIR__ . "/vorbereiten.php";

$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (:titel)");

$einfuegen->execute([":titel" => "Zucker"]);

echo "Die neue Nummer ist: ";
var_dump($db->lastInsertId());

echo "Als Zahl gebraucht, also mit (int) davor: ";
var_dump((int) $db->lastInsertId());

$einfuegen->execute([":titel" => "Mehl"]);
$einfuegen->execute([":titel" => "Salz"]);

echo "\nNach zwei weiteren steht dort die letzte, nicht die erste:\n";
echo "  ", $db->lastInsertId(), "\n";

echo "\nUnd nach einem UPDATE steht dort immer noch dieselbe Zahl:\n";
$db->prepare("UPDATE notizen SET erledigt = 1 WHERE id = :id")->execute([":id" => 1]);
echo "  ", $db->lastInsertId(), "\n";

$db->lastInsertId() gibt die Nummer der zuletzt eingefügten Zeile zurück. Du brauchst sie öfter, als du denkst: um gleich danach auf die neue Notiz weiterzuleiten, um sie in einer zweiten Tabelle zu verknüpfen, oder um sie dem Benutzer zu nennen.

Zwei Dinge daran solltest du wissen. Erstens ist der Rückgabewert eine Zeichenkette und keine Zahl, das siehst du im var_dump als string(1) "4". Das liegt daran, dass eine Datenbank auch Schlüssel vergeben kann, die in kein PHP-Integer passen. Brauchst du wirklich eine Zahl, schreibst du (int) davor.

Ganz ehrlich: Zwingend nötig ist das (int) meistens nicht. Eine Funktion mit dem Rückgabetyp int nimmt die Zeichenkette "4" klaglos an und macht selbst eine 4 daraus, weil PHP ohne declare(strict_types=1) zwischen Zahl und Zahlentext hin und her wandelt. Mit strict_types dagegen ist genau das ein TypeError. Schreib das (int) trotzdem hin: Es kostet nichts und macht sichtbar, dass hier eine Umwandlung passiert, statt sie PHP zu überlassen.

Zweitens heißt „zuletzt” wirklich zuletzt. Fügst du drei Zeilen ein, bekommst du danach die Nummer der dritten. Willst du alle drei Nummern, holst du sie jeweils direkt nach ihrem execute() ab. Ein UPDATE dazwischen ändert den Wert übrigens nicht, dort wird ja nichts eingefügt.

Hat das etwas getroffen?

Wie viele Zeilen hat das getroffen?
<?php

// Wie viele Zeilen hat das getroffen?

require __DIR__ . "/vorbereiten.php";

$erledigen = $db->prepare("UPDATE notizen SET erledigt = 1 WHERE id = :id");

$erledigen->execute([":id" => 2]);
echo "UPDATE auf Notiz 2:   ", $erledigen->rowCount(), "\n";

$erledigen->execute([":id" => 2]);
echo "Noch einmal dieselbe: ", $erledigen->rowCount(), "\n";

$erledigen->execute([":id" => 99]);
echo "UPDATE auf Notiz 99:  ", $erledigen->rowCount(), "\n";

$loeschen = $db->prepare("DELETE FROM notizen WHERE id = :id");
$loeschen->execute([":id" => 3]);
echo "DELETE auf Notiz 3:   ", $loeschen->rowCount(), "\n";

$alle = $db->query("SELECT * FROM notizen");

echo "\nUnd bei einem SELECT:\n";
echo "  rowCount() sagt:  ", $alle->rowCount(), "\n";
echo "  wirklich da sind: ", count($alle->fetchAll()), "\n";

$stmt->rowCount() beantwortet nach einem UPDATE oder DELETE die Frage, wie viele Zeilen betroffen waren. Meistens ist die Antwort 1, und interessant ist der Fall, in dem sie 0 ist: Dann gab es die Zeile nicht, und dein Programm sollte etwas anderes tun, als so zu tun, als hätte es geklappt.

Achte auf die zweite Zeile im Beispiel. Dieselbe Notiz wird zweimal auf erledigt gesetzt, und beim zweiten Mal steht dort trotzdem 1. rowCount() zählt die Zeilen, auf die das WHERE gepasst hat, nicht die Zeilen, in denen sich wirklich ein Wert geändert hat. Das ist meistens die Auskunft, die man will, nur eben nicht die, die man erwartet.

Und ein Sonderfall, der Zeit kostet, wenn man ihn nicht kennt: Nach einem SELECT ist rowCount() bei SQLite nicht zu gebrauchen. Im Beispiel sagt es 0, obwohl zwei Zeilen da sind. Die Zahl der Treffer bekommst du entweder mit count($stmt->fetchAll()) oder, wenn du die Zeilen gar nicht brauchst, mit einer eigenen Abfrage SELECT count(*) FROM ....

Das vergessene WHERE

Ein vergessenes WHERE
<?php

// Ein vergessenes WHERE. Sieh dir die zweite Tabelle an.

require __DIR__ . "/vorbereiten.php";

function zeigen(PDO $db, string $wann): void
{
    echo $wann, "\n";

    foreach ($db->query("SELECT id, titel, erledigt FROM notizen ORDER BY id") as $zeile) {
        echo "  ", $zeile["id"], "  ", str_pad($zeile["titel"], 8), "erledigt=", $zeile["erledigt"], "\n";
    }

    echo "\n";
}

zeigen($db, "Vorher:");

$db->exec("UPDATE notizen SET erledigt = 1");

zeigen($db, "Nach UPDATE notizen SET erledigt = 1 ohne WHERE:");

UPDATE notizen SET erledigt = 1 ohne WHERE ist eine vollkommen gültige Anweisung. Sie setzt erledigt in jeder Zeile auf 1. SQLite fragt nicht nach, es gibt keine Warnung, und rückgängig machen kannst du es nicht. Dasselbe gilt für DELETE FROM notizen, nur ist danach die ganze Tabelle leer.

Das ist kein hypothetischer Fehler. Er passiert, wenn man eine Abfrage in einem Datenbankwerkzeug zusammenbaut und zu früh auf Ausführen klickt, und er passiert im Code, wenn eine Variable leer ist und das WHERE aus einer Bedingung zusammengesetzt wurde.

Zwei Gewohnheiten helfen wirklich. Schreib das WHERE zuerst und den SET-Teil danach, dann kann es nicht fehlen. Und probier eine Änderung, bei der du unsicher bist, erst als SELECT mit demselben WHERE aus: Was der SELECT anzeigt, ist genau das, was das UPDATE treffen wird.

Löschen, oder lieber doch nicht

Wirklich löschen, oder nur als gelöscht markieren
<?php

// Wirklich loeschen, oder nur als geloescht markieren.

require __DIR__ . "/vorbereiten.php";

$db->exec("ALTER TABLE notizen ADD COLUMN geloescht_am TEXT");

echo "Wirklich loeschen. Danach ist die Zeile weg:\n";

$db->prepare("DELETE FROM notizen WHERE id = ?")->execute([1]);

echo "  Zeilen in der Tabelle: ", $db->query("SELECT count(*) FROM notizen")->fetchColumn(), "\n";
echo "  Notiz 1 noch da:       ";
var_dump($db->query("SELECT * FROM notizen WHERE id = 1")->fetch());

echo "\nAls geloescht markieren. Die Zeile bleibt, die Liste wird kuerzer:\n";

$db->prepare("UPDATE notizen SET geloescht_am = ? WHERE id = ?")->execute(["2026-08-29", 2]);

echo "  Zeilen in der Tabelle: ", $db->query("SELECT count(*) FROM notizen")->fetchColumn(), "\n";
echo "  Zeilen in der Liste:   ", $db->query("SELECT count(*) FROM notizen WHERE geloescht_am IS NULL")->fetchColumn(), "\n";
echo "  Notiz 2 sagt:          geloescht am ", $db->query("SELECT geloescht_am FROM notizen WHERE id = 2")->fetchColumn(), "\n";

DELETE entfernt die Zeile, und danach ist sie weg. Das klingt richtig und ist es oft auch. In einer Anwendung, die Menschen benutzen, ist es das öfter nicht: Jemand löscht versehentlich, jemand will wissen, was letzte Woche in der Liste stand, oder eine zweite Tabelle verweist noch auf die Zeile.

Der übliche Ausweg heißt Soft Delete und ist einfacher, als der Name klingt. Statt zu löschen, setzt du eine Spalte, etwa geloescht_am. Die Zeile bleibt liegen, und überall dort, wo du die Liste anzeigst, ergänzt du WHERE geloescht_am IS NULL.

Bezahlt wird das damit, dass du diese Bedingung nirgends vergessen darfst, sonst tauchen gelöschte Einträge wieder auf. Für die Notizliste in diesem Kurs bleiben wir deshalb beim echten DELETE. Für Rechnungen, Bestellungen und alles, was jemand nachvollziehen können muss, ist die Spalte die bessere Wahl.

IS NULL ist dabei kein Tippfehler: In SQL prüfst du auf einen leeren Wert mit IS NULL und nicht mit = NULL. Ein Vergleich mit NULL ergibt nämlich weder wahr noch falsch, sondern wieder NULL, und damit trifft WHERE spalte = NULL grundsätzlich keine einzige Zeile.

Zum Mitnehmen

Drei Befehle, und einer davon hat einen Ruf. Ein UPDATE ohne WHERE ändert nicht eine Zeile, sondern alle, und es fragt vorher nicht nach. Das ist keine Anekdote, das passiert regelmäßig.

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.