====== LU08c: Tabellen mit Fremdschlüssel erstellen ======
**Ziel:** Wir verteilen die Daten wie bei WordPress auf mehrere Tabellen (**users**, **posts**, **categories** und die N:M-Zuordnung **post_category**) **und setzen die Fremdschlüssel direkt beim Erstellen**.
==== ERD (Überblick) ====
Wir gehen vom Schema aus dem Reiseblog-Beispiel aus:
{{ :modul:m290_guko:learningunits:lu08:theorie:crowsfoot_wordpress_reiseblog.drawio.png?nolink&1100 | ERD: Blog Post – User (1:N) und Blog Post – Category (N:M) }}
Posts können mehreren Kategorien angehören (N:M). Die saubere Lösung ist eine Zwischentabelle ''post_category''.
Das bauen wir später (s. LU08e: N:M-Beziehungen mit Zwischentabelle abbilden).
===== Fremdschlüssel: Grundsyntax =====
[[https://www.youtube.com/watch?v=QDmJBSgnUEo|SQL Schlüssel einfach erklärt – SQL 3]]((Informatik – simpleclub / YouTube)) -> (7:49, de) Primär- und Fremdschlüssel anschaulich erklärt.
CREATE TABLE table_name (
id INT AUTO_INCREMENT PRIMARY KEY,
foreign_key_col INT NOT NULL, -- Datentyp muss zum referenzierten Primary Key (in der anderen Tabelle) passen
FOREIGN KEY (foreign_key_col) REFERENCES parent_table(parent_pk)
);
**Wichtig:** Die **referenzierte Tabelle** (parent_table) muss **bereits existieren**, sonst meldet MySQL/MariaDB einen Fehler.
===== Beispiel Reiseblog =====
[[https://www.youtube.com/watch?v=i-W5bos0LlE|SQL Tabellenerzeugung mit DDL – SQL 2]]((Informatik – simpleclub / YouTube)) -> (7:46, de) Kurz und klar: CREATE TABLE, Datentypen, Constraints – ideal als DDL-Auffrischung vor dem Setzen von FKs.
==== 1. Tabellen anlegen ====
=== Tabelle users ===
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(60) NOT NULL UNIQUE,
email VARCHAR(191) NOT NULL UNIQUE,
display_name VARCHAR(100) NOT NULL,
registered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
=== Tabelle posts ===
CREATE TABLE posts (
post_id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
featured_img VARCHAR(512),
content TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
author_id INT NOT NULL,
FOREIGN KEY (author_id) REFERENCES users (user_id) -- Foreign Key wird hier angelegt!
);
=== Tabelle categories ===
CREATE TABLE categories (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(120) NOT NULL UNIQUE -- z. B. 'schweiz', 'staedtereise'
);
==== 2. Beispieldaten einfügen ====
//Für ausführliche Erklärung zum Einfügen von Daten in Tabellen via SQL schauen Sie in "LU07 - DML: Daten einfügen, ändern und löschen" nach.//
-- 1) Autor:innen
INSERT INTO users (username, email, display_name) VALUES
('caro', 'caro@wetraveltheworld.de', 'Caro Steig'),
('martin', 'martin@wetraveltheworld.de', 'Martin Merten'),
('shaolin', 'shaolin@wetraveltheworld.de', 'Shaolin Tran');
-- 2) Kategorien
INSERT INTO categories (name, slug) VALUES
('Städtereise', 'staedtereise'),
('Abenteuer', 'abenteuer'),
('Roadtrip', 'roadtrip'),
('Belgien', 'belgien'),
('Niederlande', 'niederlande'),
('Portugal', 'portugal'),
('Deutschland', 'deutschland'),
('Montenegro', 'montenegro'),
('Oman', 'oman'),
('USA', 'usa');
-- 3) Posts
INSERT INTO posts (title, featured_img, content, created_at, author_id) VALUES
('Hasselt – 10 Highlights',
'https://wetraveltheworld.de/wp-content/uploads/2025/05/hasselt.jpg',
'Kurzguide: Die schönsten Ecken von Hasselt …',
'2025-05-07 10:15:00', 2), -- Martin
('Utrecht – 10 Sehenswürdigkeiten',
'https://wetraveltheworld.de/wp-content/uploads/2025/06/utrecht.jpg',
'Cafés, Grachten und Restaurant-Tipps …',
'2025-06-05 09:30:00', 2), -- Martin
('Lissabon – 8 Tipps zu den wichtigsten Sehenswürdigkeiten',
'https://wetraveltheworld.de/wp-content/uploads/2025/03/lissabon.jpg',
'Aussichtspunkte, Viertel und Highlights …',
'2025-03-21 08:40:00', 1), -- Caro
('Maastricht an einem Tag',
'https://wetraveltheworld.de/wp-content/uploads/2025/06/maastricht.jpg',
'Spaziergang, Restaurants, Altstadt …',
'2025-06-12 11:05:00', 1), -- Caro
('Montenegro Roadtrip – 10 Highlights',
'https://wetraveltheworld.de/wp-content/uploads/2025/06/montenegro.jpg',
'Bucht von Kotor, Durmitor, Tara-Schlucht …',
'2025-06-11 14:22:00', 1), -- Caro
('Oman – Top 22 Highlights',
'https://wetraveltheworld.de/wp-content/uploads/2025/06/oman.jpg',
'Nizwa, Wadis, Wüste und Roadtrip-Tipps …',
'2025-06-11 09:00:00', 1), -- Caro
('Chicago in 3 Tagen – 17 Highlights',
'https://wetraveltheworld.de/wp-content/uploads/2024/10/chicago.jpg',
'Riverwalk, The Bean, Museen und Skyline …',
'2024-10-07 07:50:00', 2); -- Martin
**Resultate (nach dem Einfügen):**
=== Verknüpfung Tabelle users & posts (one-to-many) ===
{{:modul:m290_guko:learningunits:lu08:theorie:reiseblog-connected_users-posts.png?direct&1600|}}
=== Tabelle categories (wird später mit 'posts' verknüpft) ===
{{:modul:m290_guko:learningunits:lu08:theorie:categories.png?direct&400|}}
==== 3. Fremdschlüssel in Aktion (Standard: RESTRICT) ====
Beim Setzen von Fremdschlüsseln überwacht MySQL/MariaDB Änderungen an den verknüpften Spalten und verhindert Operationen, die zu inkonsistenten Verweisen führen. Diese Reaktion heisst //Referenzaktion//. Standardmässig gilt ''RESTRICT'': Eine Zeile in der Elterntabelle darf nicht gelöscht werden und ihr Primärschlüssel darf nicht geändert werden, solange Kindzeilen auf sie zeigen.
Bezogen auf unser Reiseblog-Beispiel: ''posts.author_id'' verweist auf ''users.user_id''.
Damit ist //users// die Elterntabelle und //posts// die Kindtabelle. Die Folge von ''RESTRICT'':
* Löschen eines Users ist blockiert, solange Posts auf diesen User verweisen.
* Ändern von ''users.user_id'' ist blockiert, solange es verweisende ''posts.author_id'' gibt.
* Änderungen an nicht referenzierten Spalten (z. B. ''users.username'') sind weiterhin erlaubt.
Probieren Sie folgende Codesnippets in Webstorm/MySQL, damit Sie gleich das entsprechende Gefühl dafür bekommen, was //RESTRICT// passiert.
=== Demo 1 – User ohne Posts löschen (erlaubt) ===
DELETE FROM users
WHERE username = 'shaolin'; -- hat keine Posts zugewiesen
SELECT user_id, username FROM users;
//Erwartung: Die Zeile wird gelöscht (keine Posts verweisen auf den User).//
=== Demo 2 – User mit Posts löschen (blockiert) ===
DELETE FROM users
WHERE username = 'martin';
//Erwartete Fehlermeldung (sinngemäss)://
[23000][1451] Cannot delete or update a parent row: a foreign key constraint fails
(travel_blog.posts, CONSTRAINT posts_ibfk_1 FOREIGN KEY (author_id) REFERENCES users(user_id))
//Grund: In posts.author_id gibt es Kindzeilen (z.B. "Hasselt – 10 Highlights"), die auf users.user_id von martin verweisen → RESTRICT verhindert das Löschen.//
=== Demo 3 – Unkritisches Attribut ändern (erlaubt) ===
UPDATE users
SET username = 'martin_travels'
WHERE user_id = 2; -- OK: FKs verweisen auf user_id, nicht auf username
=== Demo 4 – Primärschlüssel ändern (blockiert) ===
UPDATE users
SET user_id = 5
WHERE user_id = 2; -- erwartet: Fehler (RESTRICT), da posts.author_id -> users.user_id
//Erklärung: posts.author_id → users.user_id ist ein FK mit Standard ON UPDATE RESTRICT. Solange Posts existieren, darf der user_id-Wert nicht verändert werden.//
=== Demo 5 – Primärschlüssel ändern: Geht das? (ja) ===
UPDATE categories
SET category_id = 11
WHERE category_id = 10; -- USA → 11: funktioniert - Primary Keys dürfen geändert werden.
=== Warum ist das so? ===
//RESTRICT// (Standard) schützt die Elternzeile (z. B. users): Solange Kindzeilen (z. B. posts) auf sie zeigen, sind DELETE/UPDATE am referenzierten Primärschlüssel blockiert.
Änderungen an nicht referenzierten Spalten (z. B. ''users.username'') sind frei möglich – FKs verweisen hier nicht darauf.
Ob eine Änderung blockiert oder mitgezogen (CASCADE) wird, hängt von der ON DELETE/ON UPDATE-Einstellung im FK ab.
Merke: Fremdschlüssel geben dir Datensicherheit:
* Verhindern verwaiste Daten (z. B. Posts ohne gültigen Autor),
* definieren klares Verhalten bei Löschen/Ändern (RESTRICT, CASCADE, SET NULL),
* halten die Datenbank konsistent.
Ausblick: In **LU08d** fügen wir FKs per **ALTER TABLE** nachträglich hinzu und testen die Referenzaktionen **RESTRICT**, **CASCADE** und **SET NULL** gezielt.