Foreign Keys
Wir kennen bereits die Primary Keys, aber es gibt noch mindestens einen weiteren wichtigen Schlüsseltyp.
Das ist der sogenannte Foreign Key.
Er prüft lediglich, ob ein Wert in einer Spalte in einer Spalte einer anderen Tabelle vorhanden ist.
Klingt kompliziert? Glaub mir, das ist es nicht!
Erinnerst du dich an unser Geldproblem in Kapitel 2, bei dem wir einen Spieler gelöscht haben, sein Geld aber immer noch in der Geldtabelle aufgeführt war? Damals mussten wir es manuell löschen. Mit Foreign Keys können wir stattdessen etwas viel Cooleres machen.
Definieren wir unsere money Tabelle neu und fügen einen Foreign Key hinzu, diesmal für unsere neue player Tabelle mit dem Auto Increment und all den anderen coolen Dingen.
Natürlich werden wir auch hier unser ganzes bisheriges Wissen anwenden. Fassen wir schnell zusammen, was wir erreichen wollen:
- Jeder Spieler soll nur einmal vorkommen -> Primärschlüssel auf player_id
- Der Wert von
moneysoll anfangs 0 sein -> Standardwert ist 0 - Nur Spieler, die in unserer
playerTabelle vorhanden sind, sollen einen Geldbetrag aufgeführt haben ->Foreign Keyvon money. money.player_id zu player.id - Wenn ein Spieler gelöscht wird, soll auch der Geldeintrag gelöscht werden -> Beim Löschen wird die Löschung in anderen Tabellen kaskadiert.
Auf geht's in die Tat!
CREATE TABLE money (
-- Wir nennen die id player_id, weil sie auf die Spalte id in der Tabelle player verweist.
-- Da dies der Primärschlüssel ist, brauchen wir ihn nicht auf NOT NULL zu setzen
player_id INT PRIMARY KEY,
-- Wir definieren unsere money-Spalte und setzen den Standardwert auf 0.
money DECIMAL DEFAULT 0 NOT NULL,
-- Wir definieren den Namen unseres Foreign keys. Das Benennungsschema ist einfach
-- <curr_table>_<target_table>_<curr_col>_<target_col>_fk
-- Gegenseitig spielen die Namen keine Rolle, aber sie müssen eindeutig sein, daher ist es üblich, sie so zu benennen.
CONSTRAINT money_player_player_id_fk
-- Wir definieren einen Foreign key für unsere player_id-Spalte und binden ihn an die id-Spalte der Spielertabelle.
FOREIGN KEY (player_id) REFERENCES player (id)
-- Falls die id in unserer Spielertabelle gelöscht wird, wollen wir sie
ON DELETE CASCADE
);
Und das war's auch schon.
Zeit zum Herumspielen.
Zurzeit haben wir bereits eine Reihe von Spielern in unserer player Tabelle:
| id | player_name | last_online |
|---|---|---|
| 1 | Mike | 2022-11-26 12:32:39.021491 |
| 2 | Sarah | 2022-11-26 12:32:39.021491 |
| 3 | John | 2022-11-26 12:32:39.021491 |
| 4 | Lilly | 2022-11-26 12:32:39.021491 |
| 5 | Matthias | 2022-11-26 12:32:39.021491 |
| 6 | Lenny | 2022-11-26 12:32:39.021491 |
| 7 | Summer | 2022-11-26 12:32:39.021491 |
| 8 | Marry | 2022-11-26 12:32:39.021491 |
| 9 | Milana | 2022-11-26 12:32:39.021491 |
| 10 | Lexi | 2022-11-26 12:32:39.021491 |
Überprüfe, dass wir keine unbekannten Spieler einfügen können
Dies schlägt fehl, weil wir keinen Spieler mit der ID 11 in unserer player Tabelle haben.
Der Foreign Key verhindert, dass wir unbekannte Spieler einfügen können.
Überprüfe, ob wir bekannte Spieler einfügen können
| player_id | money |
|---|---|
| 10 | 0 |
Es scheint zu funktionieren!
Wir haben einen Spieler mit der ID 10 hinzugefügt, welche die ID von Lexi ist, und der Wert von money wurde automatisch auf 0 gesetzt.
Überprüfe, dass der Eintrag money gelöscht wird, wenn wir einen Spieler löschen
Jetzt erhalten wir nichts mehr, wenn wir die Tabelle money lesen.
Das ist großartig!
Lexis Eintrag wurde in dem Moment gelöscht, in dem wir den Eintrag in der player Tabelle gelöscht haben.
Komplexere Foreign keys
Jetzt haben wir ein gutes Verständnis für einen einfachen Foreign Key auf einer einzelnen Spalte, aber wir haben noch eine komplexere Aufgabe zu lösen.
Wir haben immer noch unseren friend_graph, der immer noch Freundschaftsverbindungen von nicht existierenden Spielern enthalten kann.
Das ist ein Problem, das wir jetzt lösen wollen, und es wird ein bisschen komplexer sein.
Unsere letzte Tabelle war ganz einfach: Für jeden Eintrag in der player Tabelle konnten wir nur einen Eintrag in unserer money Tabelle haben.
Aber der friend_graph enthält mehrere Einträge für einen einzigen Spieler und sogar in zwei Spalten und nicht nur in einer!
Nur für den Fall, dass du nicht mehr weißt, wie unsere Tabelle aussieht:
| player_1 | player_2 |
|---|---|
| 1 | 2 |
| 2 | 3 |
| 4 | 3 |
| 5 | 3 |
| 7 | 2 |
| 6 | 1 |
| 6 | 2 |
| 1 | 10 |
| 4 | 10 |
Wir können keinen Primärschlüssel für Spieler 1 ODER 2 verwenden, aber wir können trotzdem einen Primärschlüssel für Spieler 1 UND 2 verwenden.
CREATE TABLE friend_graph (
player_id_1 INT,
player_id_2 INT,
-- Wir definieren unseren Primärschlüssel
CONSTRAINT friend_graph_pk
PRIMARY KEY (player_id_1, player_id_2),
-- Wir definieren wieder unsere Referenz und legen das Löschen fest
CONSTRAINT friend_graph_player_player_id_1_id_fk
FOREIGN KEY (player_id_1) REFERENCES player (id)
ON DELETE CASCADE,
-- Wir definieren unsere Referenz erneut und legen das Löschen fest
CONSTRAINT friend_graph_player_player_id_2_id_fk
FOREIGN KEY (player_id_2) REFERENCES player (id)
ON DELETE CASCADE
);
Und das war's auch schon.
Immer noch ziemlich einfach.
Anstatt einen Foreign Key für eine einzelne Spalte zu erstellen, legen wir einfach zwei für jede Spalte an.
Du kannst gerne mit ähnlichen Tests wie oben überprüfen, ob es funktioniert!
Wir haben hier immer noch ein Problem in Bezug auf die Konsistenz.
Wir können hier einen doppelten Eintrag haben, da player_1 ein Freund von player_2 und player_2 ein Freund von player_1 sein kann.
Das wird durch den Primärschlüssel nicht verhindert.
| player_1 | player_2 |
|---|---|
| 1 | 2 |
| 2 | 1 |
Es gibt mehrere Möglichkeiten, dieses Problem zu lösen.
Wir könnten entweder immer die niedrigere ID in player_1 und die höhere in player_2 einfügen oder ein XOR auf beide IDs anwenden, um einen eindeutigen Schlüssel für die Freundschaft zu erstellen.
Im Moment machen wir uns damit nicht die Mühe, weil uns für beides das Wissen fehlt.