Webentwicklung

Fremdschlüssel in MariaDB - Unmögliche Beziehungen von der Datenbank ablehnen lassen

Fremdschlüssel in MariaDB - Unmögliche Beziehungen von der Datenbank ablehnen lassen

Ein Datensatz für einen Beitrag gibt Benutzer 42 als Autor an, doch Benutzer 42 existiert nicht. Die Anwendung kann den fehlenden Namen ausblenden, einen Fehler zurückgeben oder stillschweigend „unbekannter Autor“ anzeigen. Keine dieser Reaktionen repariert jedoch die in der Datenbank gespeicherte Beziehung. Die schwierigere Frage lautet, wo dieser unmögliche Zustand abgewiesen werden sollte.

Validierung in der Anwendung ist nützlich, aber ein Fremdschlüssel verankert die Regel an der gemeinsamen Datengrenze. Jeder Schreibzugriff auf die Datenbank muss dann dieselbe Beziehung einhalten, unabhängig davon, ob er aus PHP, einem Wartungsskript, einem Import oder einer zweiten Anwendung stammt. Das ist eine starke, aber eng begrenzte Garantie: Ein Fremdschlüssel kann sicherstellen, dass ein referenzierter Datensatz existiert. Er kann nicht beweisen, dass der aktuelle Benutzer darauf verweisen darf, dass der Inhalt gültig ist oder dass eine Löschung sinnvoll wäre.

Dieser Artikel entwickelt diese Grenze für eine kleine PHP-Anwendung mit MariaDB und InnoDB. Die entscheidende Arbeit besteht nicht nur darin, FOREIGN KEY hinzuzufügen. Es muss festgelegt werden, was die Beziehung bedeutet, was geschehen soll, wenn ihr übergeordneter Datensatz verschwindet, und wie die Durchsetzung eingeführt werden kann, ohne bereits inkonsistente Daten zu ignorieren.

Der Constraint beschreibt genau eine Invariante

Angenommen, eine Anwendung speichert Autoren und Beiträge. Die übergeordnete Tabelle enthält die Autorenkennungen; die untergeordnete Tabelle speichert eine dieser Kennungen in posts.author_id. Laut der MariaDB-Dokumentation zu Fremdschlüsseln muss jeder untergeordnete Wert, der nicht NULL ist, mit einem Wert im referenzierten übergeordneten Schlüssel übereinstimmen.

CREATE TABLE authors (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE posts (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  author_id BIGINT UNSIGNED NULL,
  title VARCHAR(191) NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT fk_posts_author
    FOREIGN KEY (author_id) REFERENCES authors (id)
    ON DELETE SET NULL
    ON UPDATE RESTRICT
) ENGINE=InnoDB;

Mit dieser Definition wird ein Insert abgewiesen, das eine unbekannte, nicht als NULL gesetzte author_id enthält. MariaDB dokumentiert diesen Fall als Fehler 1452 mit SQLSTATE 23000. Der Constraint besitzt außerdem bewusst einen Namen. MariaDB kann einen Namen erzeugen, doch ein stabiler Name macht Fehlermeldungen, Schemaanalysen und spätere Migrationen leichter verständlich.

Das Beispiel erlaubt für author_id den Wert NULL. Das ist eine Entscheidung des Datenmodells und keine Anforderung des Fremdschlüssels. Hier bedeutet sie, dass ein Beitrag ohne aktuelle Autorenbeziehung bestehen bleiben darf. Wenn jeder Beitrag stets einen Autor haben muss, sollte die Spalte NOT NULL sein und die Löschrichtlinie muss diese Regel erhalten.

Warum eine Existenzprüfung in PHP nicht dieselbe Garantie bietet

Ein Handler kann vor dem Einfügen eines Beitrags die Tabelle authors abfragen. Das ermöglicht einen verständlicheren Validierungsweg, ersetzt den Constraint jedoch nicht. Ein anderer Datenzugriff könnte die Prüfung auslassen. Weniger offensichtlich ist, dass der Autor nach der Prüfung und vor dem Insert gelöscht werden kann. Die beiden Anweisungen beobachten unterschiedliche Zeitpunkte, sofern die Anwendung sie nicht mit geeigneten Datenbankregeln und passendem Transaktionsverhalten koordiniert.

Der Fremdschlüssel bewertet die Beziehung dort, wo der Schreibvorgang angenommen wird. Die Anwendungsvalidierung kann das Problem weiterhin vor dem Schreibversuch erklären, während die Datenbank die letzte Integritätsgrenze bleibt. Aus demselben Grund darf ein Formular prüfen, ob ein Benutzername verfügbar scheint, während ein Unique Constraint entscheidet, ob zwei gleichzeitige Registrierungen ihn tatsächlich beanspruchen können.

Das bedeutet nicht, dass jeder Datenbankfehler als Meldung der Benutzeroberfläche geeignet ist. Ein unverarbeiteter Constraint-Name oder ein SQL-Statement gehört in geschützte Diagnosedaten, nicht in eine öffentliche Antwort. Die Anwendung sollte einen erwarteten Konflikt in die Sprache ihrer Domäne übersetzen, etwa „Wählen Sie einen vorhandenen Autor“. Unerwartete Fehler folgen dagegen der üblichen serverseitigen Fehlerbehandlung.

Löschaktionen sind Richtlinien und keine Komfortschalter

Die folgenreichste Zeile ist häufig ON DELETE. MariaDB unterstützt für gewöhnliche InnoDB-Fremdschlüssel RESTRICT, CASCADE und SET NULL. In MariaDB ist NO ACTION ein Synonym für RESTRICT; eine ausgelassene Aktion verwendet standardmäßig RESTRICT. Diese Details sind produktspezifisch. Eine andere Datenbank kann NO ACTION eine andere zeitliche Semantik geben.

  • RESTRICT weist das Löschen eines übergeordneten Datensatzes ab, solange untergeordnete Datensätze darauf verweisen. Das ist eine vorsichtige Vorgabe, wenn das untergeordnete Objekt eine eigene Historie besitzt oder das Löschen einen ausdrücklichen Ablauf erfordert.
  • CASCADE löscht passende untergeordnete Datensätze automatisch. Das eignet sich für Datensätze, die tatsächlich Bestandteile des übergeordneten Objekts sind, etwa Elemente in einem temporären Entwurf, die nach dem Löschen des Entwurfs keine Bedeutung mehr haben.
  • SET NULL erhält jeden untergeordneten Datensatz, entfernt aber die Beziehung. Dafür müssen die untergeordneten Spalten nullable sein und „derzeit kein übergeordnetes Objekt“ muss eine wahrheitsgemäße Bedeutung besitzen.

Die PostgreSQL-Dokumentation zu Constraints bietet eine hilfreiche allgemeine Designunterscheidung: Cascading passt, wenn das untergeordnete Objekt eine Komponente ist, die allein keinen sinnvollen Bestand hat. Restriction eignet sich eher, wenn übergeordnetes und untergeordnetes Objekt unabhängige Dinge darstellen. Das ist konzeptionelle Orientierung und keine Dokumentation des MariaDB-Verhaltens.

Bei veröffentlichten Inhalten kann eine automatische Löschung zu destruktiv sein, selbst wenn sie technisch gültig ist. Einen Beitrag mit nullable Autor zu erhalten, das Löschen des Autors zu verhindern oder einen Deaktivierungsstatus auf Anwendungsebene zu verwenden, kann jeweils ehrlicher sein. Ein Cascade sollte eine geklärte Lebenszyklusregel abbilden und nicht bloß ein zusätzliches DELETE-Statement einsparen.

Die Spalten müssen übereinstimmen

Eine Beziehung kann konzeptionell richtig sein und dennoch auf Schemaebene scheitern. MariaDB verlangt, dass übergeordnete und untergeordnete Tabelle dieselbe unterstützte Storage Engine und kompatible Spaltendefinitionen verwenden. Bei Integer-Schlüsseln müssen Größe und Vorzeichen übereinstimmen. Ein übergeordneter BIGINT UNSIGNED sollte nicht mit einem vorzeichenbehafteten BIGINT im untergeordneten Datensatz kombiniert werden. Bei String-Schlüsseln müssen auch Zeichensatz und Collation übereinstimmen.

Die referenzierten übergeordneten Spalten müssen indiziert sein. Die untergeordneten Spalten benötigen einen BTREE-Index oder müssen den linken Anfang eines solchen Index bilden. InnoDB kann den untergeordneten Index bei Bedarf erzeugen, doch die Abhängigkeit von diesem Nebeneffekt kann die beabsichtigte Indexstruktur verschleiern. Zusammengesetzte Beziehungen erfordern die Spalten in der richtigen Reihenfolge. Präfixindizes und damit gewöhnliche Präfixe von TEXT oder BLOB können nach den dokumentierten MariaDB-Regeln nicht als Fremdschlüsselspalten dienen.

Fremdschlüssel implizieren außerdem kein NOT NULL. MariaDB erlaubt einen untergeordneten Datensatz mit dem Fremdschlüsselwert NULL. Das Schema muss mit der Nullability ausdrücken, ob „keine Beziehung“ gültig ist, statt anzunehmen, der Constraint beantworte diese Frage.

Vorhandene Daten müssen die Regel erfüllen, bevor sie helfen kann

Das Hinzufügen eines Constraints zu einer bestehenden Anwendung beginnt mit Beobachtung. Zuerst sollten mit SHOW CREATE TABLE die tatsächlichen Tabellendefinitionen geprüft werden, einschließlich Engine, Schlüsseltypen, Vorzeichen, Zeichensätze, Collations und vorhandener Indizes. Danach werden untergeordnete Datensätze gesucht, welche die geplante Beziehung verletzen würden:

SELECT p.id, p.author_id
FROM posts AS p
LEFT JOIN authors AS a ON a.id = p.author_id
WHERE p.author_id IS NOT NULL
  AND a.id IS NULL;

Ein leeres Ergebnis spricht für das Hinzufügen des Constraints; es wählt noch nicht die richtige Löschaktion aus. Ein nicht leeres Ergebnis zeigt, dass die Anwendung eine Datenentscheidung benötigt. Je nach Domäne kann die richtige Reparatur darin bestehen, ein fehlendes übergeordnetes Objekt wiederherzustellen, das untergeordnete Objekt einem gültigen Objekt zuzuordnen, eine optionale Referenz auf NULL zu setzen, den Datensatz zur Prüfung zu isolieren oder nachweislich ungültige Daten zu löschen. Eine automatische Auswahl ohne Verständnis der Datensätze lässt lediglich die Abfrage verstummen.

Sobald Daten und Definitionen übereinstimmen, kann die Migration den benannten Constraint hinzufügen:

ALTER TABLE posts
  ADD CONSTRAINT fk_posts_author
  FOREIGN KEY (author_id) REFERENCES authors (id)
  ON DELETE SET NULL
  ON UPDATE RESTRICT;

Dies ist produktives DDL und keine folgenlose Textänderung. Die MariaDB-Dokumentation zu ALTER TABLE weist darauf hin, dass die Anweisung auf einen Metadata Lock warten kann und verfügbare Algorithmen sowie Locking-Strategien von Operation, Engine und Serverversion abhängen. Bei einer großen oder stark genutzten Tabelle sollten die Unterlagen der installierten Version geprüft, repräsentative Daten getestet, ein Rollback geplant und die Migration beobachtet werden, statt von einer sofortigen Ausführung auszugehen.

Deaktivierte Prüfungen sind keine Bereinigungsstrategie

MariaDB stellt foreign_key_checks bereit. Eine Deaktivierung kann bei kontrollierten Massenimporten oder Schemaverfahren nützlich sein. Sie erleichtert jedoch auch das Erzeugen von Daten, welche die deklarierte Beziehung normalerweise zurückweisen würde. Das erneute Aktivieren der Prüfungen ersetzt kein Audit auf verwaiste Datensätze. Die MariaDB-Dokumentation warnt zudem, dass das Hinzufügen eines Constraints bei deaktivierten Prüfungen vorhandene Zeilen unvalidiert lassen kann.

Eine sicherere Migrationsgeschichte ist ausdrücklich: Beziehungen inventarisieren, Verletzungen erkennen, die Korrektur jeder Verletzungsklasse festlegen, das DDL in einem geplanten Betriebsfenster ausführen und anschließend die Metadaten prüfen. Falls ein Restore-Prozess das Prüfverhalten vorübergehend ändert, gehört sein Validierungsschritt in das Restore-Design und nicht in eine unausgesprochene Annahme.

PHP sollte die Domäne behandeln, nicht Datenbankprosa zerlegen

PDO verwendet laut dem PHP-Handbuch zur PDO-Fehlerbehandlung seit PHP 8.0 standardmäßig den Exception-Modus. Eine PDOException stellt SQLSTATE-Informationen und treiberspezifische Details bereit. Damit besitzt eine Anwendung genügend Struktur, um den Fehler zu protokollieren und bei einem bekannten Konfliktpfad eine kontrollierte Antwort der Domäne zurückzugeben.

Zentrales Verhalten sollte nicht durch den Vergleich menschenlesbarer Fehlermeldungen entstehen. Der Wortlaut enthält Engine-Details und kann sich ändern. Selbst SQLSTATE 23000 beschreibt eine Klasse von Integritätsverletzungen und nicht eine einzelne Geschäftsbedingung. Daher können treiberspezifische Codes oder eine vorherige Domänenprüfung weiterhin nötig sein, um ein unbekanntes übergeordnetes Objekt von einem anderen Constraint-Fehler zu unterscheiden. Die Datenbank bleibt maßgeblich, während die Anwendung für hilfreiche Sprache, Autorisierung und sichere Offenlegung verantwortlich bleibt.

Transaktionen bleiben wichtig, wenn eine Operation mehrere Datensätze ändert. Ein Fremdschlüssel verhindert eine defekte Referenz; er macht einen mehrstufigen Veröffentlichungs-, Abrechnungs- oder Benachrichtigungsablauf nicht atomar. Ebenso wenig kann er eine Beziehung zu einem Objekt durchsetzen, das nur in einem anderen Dienst existiert. Diese Grenzen benötigen ein eigenes Protokoll, eine Abstimmung oder ein Zustandsmodell.

Die deklarierte Beziehung prüfen

Nach dem Deployment sollte mehr als ein erfolgreicher Insert geprüft werden. Ein kompakter Testplan umfasst:

  • Ein untergeordnetes Objekt mit gültigem übergeordnetem Objekt einfügen und den Erfolg bestätigen.
  • Ein untergeordnetes Objekt mit unbekanntem übergeordnetem Objekt einfügen oder aktualisieren und die Abweisung bestätigen.
  • Das gewählte Verhalten beim Löschen des übergeordneten Objekts ausführen und alle betroffenen Tabellen prüfen.
  • Bestätigen, dass NULL entsprechend dem Modell angenommen oder abgewiesen wird.
  • Gleichzeitige Anwendungspfade testen, die zusammengehörige Datensätze erstellen oder entfernen.
  • Die installierte Definition mit SHOW CREATE TABLE prüfen.
  • information_schema.KEY_COLUMN_USAGE abfragen, um die eingeschränkten und referenzierten Spalten zu bestätigen.
  • Sicherstellen, dass öffentliche Fehler weder SQL noch Constraint-Namen oder private Datensatzdetails offenlegen.

Die Tests sollten auch die Wiederherstellung von Backups und Schemamigrationen abdecken. Ein Constraint, der nur in der Live-Datenbank, aber nicht in Migrationsdateien oder geprüften Backups existiert, ist ein operativer Zufall, der später erneut entdeckt werden muss.

Fazit

Ein MariaDB-Fremdschlüssel gibt einer kleinen PHP-Anwendung ein wertvolles Versprechen: Eine Referenz, die nicht NULL ist, kann nicht auf einen übergeordneten Datensatz zeigen, den die Datenbank nicht findet. Wenn dieses Versprechen in InnoDB liegt, schützt es die Beziehung über alle Schreibzugriffe hinweg, einschließlich eines zukünftigen Wartungsskripts, das die Validierung vergessen könnte.

Das Schlüsselwort ist der einfache Teil. Ein dauerhaftes Design entsteht durch übereinstimmende Spaltendefinitionen, eine bewusste Entscheidung darüber, ob Abwesenheit erlaubt ist, ein Löschverhalten auf Grundlage des Datenlebenszyklus, die Bereinigung vorhandener verwaister Datensätze und die Behandlung von ALTER TABLE als betriebliche Änderung. Fremdschlüssel ersetzen weder Anwendungsvalidierung und Autorisierung noch Transaktionen oder durchdachte Löschentscheidungen. Sie machen eine Invariante ausdrücklich und durchsetzbar. Gerade dieser engere Anspruch macht sie nützlich.

References