BREAKING CHANGE: Datenmodell-Umstellung von 1:1 auf M:N Beziehungen Migration 008: - Zielgruppen-Tabelle (target_groups) mit training_style_id Hierarchie - M:N Zuordnungstabellen: exercise_focus_areas, exercise_styles, exercise_target_groups - Altersgruppen-Dimension (exercise_age_groups) mit CHECK constraint - Hierarchische Struktur: training_styles.focus_area_id → focus_areas - Daten-Migration: Bestehende 1:1 Beziehungen zu M:N mit is_primary=true - Seed-Daten: Beispiel-Zielgruppen für Shotokan Architektur: - Smart Cascade-Logik (RESTRICT, Rerouting, Move) vorbereitet - Legacy-Spalten (focus_area_id, training_style_id) bleiben zur Rückwärtskompatibilität - Primary/Secondary Assignments via is_primary Flag Dokumentation: - .claude/docs/technical/DATABASE_SCHEMA.md (kontinuierlich gepflegt) - .claude/docs/functional/DOMAIN_MODEL.md (fachliche Anforderungen) - Migrations-Historie aktualisiert version: 0.3.0 (backend + frontend) modules: exercises 0.3.0, catalogs 1.1.0 DB_SCHEMA_VERSION: 20260423 Konzept: shinkan_anforderungsdokument_entwurf.md (§8.1, §8.3, §10.7)
166 lines
6.9 KiB
SQL
166 lines
6.9 KiB
SQL
-- Migration 008: M:N Exercise Relations + Hierarchical Catalogs
|
|
-- Erstellt: 2026-04-23
|
|
-- Beschreibung: Umstellung von 1:1 auf M:N Beziehungen + Hierarchie Fokusbereich → Stil → Zielgruppe
|
|
|
|
-- ============================================================================
|
|
-- PHASE 1: Hierarchische Struktur aufbauen
|
|
-- ============================================================================
|
|
|
|
-- training_styles erweitern: Hierarchie zu focus_areas
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
WHERE table_name='training_styles' AND column_name='focus_area_id') THEN
|
|
ALTER TABLE training_styles
|
|
ADD COLUMN focus_area_id INT REFERENCES focus_areas(id);
|
|
END IF;
|
|
END $$;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_training_styles_focus_area ON training_styles(focus_area_id);
|
|
|
|
-- Zielgruppen-Tabelle (NEU)
|
|
CREATE TABLE IF NOT EXISTS target_groups (
|
|
id SERIAL PRIMARY KEY,
|
|
training_style_id INT REFERENCES training_styles(id), -- Hierarchie-Link
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
min_age INT,
|
|
max_age INT,
|
|
sort_order INT,
|
|
status VARCHAR(50) DEFAULT 'active',
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_target_groups_style ON target_groups(training_style_id);
|
|
CREATE INDEX IF NOT EXISTS idx_target_groups_status ON target_groups(status);
|
|
|
|
-- ============================================================================
|
|
-- PHASE 2: M:N Zuordnungstabellen
|
|
-- ============================================================================
|
|
|
|
-- Übung ↔ Fokusbereiche (M:N)
|
|
CREATE TABLE IF NOT EXISTS exercise_focus_areas (
|
|
id SERIAL PRIMARY KEY,
|
|
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
|
focus_area_id INT REFERENCES focus_areas(id),
|
|
is_primary BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(exercise_id, focus_area_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_exercise ON exercise_focus_areas(exercise_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_focus ON exercise_focus_areas(focus_area_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_primary ON exercise_focus_areas(is_primary);
|
|
|
|
-- Übung ↔ Stile (M:N)
|
|
CREATE TABLE IF NOT EXISTS exercise_styles (
|
|
id SERIAL PRIMARY KEY,
|
|
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
|
training_style_id INT REFERENCES training_styles(id),
|
|
is_primary BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(exercise_id, training_style_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_styles_exercise ON exercise_styles(exercise_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_styles_style ON exercise_styles(training_style_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_styles_primary ON exercise_styles(is_primary);
|
|
|
|
-- Übung ↔ Zielgruppen (M:N)
|
|
CREATE TABLE IF NOT EXISTS exercise_target_groups (
|
|
id SERIAL PRIMARY KEY,
|
|
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
|
target_group_id INT REFERENCES target_groups(id),
|
|
is_primary BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(exercise_id, target_group_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_exercise ON exercise_target_groups(exercise_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_target ON exercise_target_groups(target_group_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_primary ON exercise_target_groups(is_primary);
|
|
|
|
-- Übung ↔ Altersgruppen (M:N - separate Dimension)
|
|
CREATE TABLE IF NOT EXISTS exercise_age_groups (
|
|
id SERIAL PRIMARY KEY,
|
|
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
|
age_group VARCHAR(50) NOT NULL, -- 'Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene'
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(exercise_id, age_group),
|
|
CHECK (age_group IN ('Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_age_groups_exercise ON exercise_age_groups(exercise_id);
|
|
CREATE INDEX IF NOT EXISTS idx_exercise_age_groups_age ON exercise_age_groups(age_group);
|
|
|
|
-- ============================================================================
|
|
-- PHASE 3: Daten-Migration (Altdaten von 1:1 zu M:N)
|
|
-- ============================================================================
|
|
|
|
-- Migriere focus_area_id → exercise_focus_areas (als primäre Zuordnung)
|
|
INSERT INTO exercise_focus_areas (exercise_id, focus_area_id, is_primary)
|
|
SELECT id, focus_area_id, true
|
|
FROM exercises
|
|
WHERE focus_area_id IS NOT NULL
|
|
ON CONFLICT (exercise_id, focus_area_id) DO NOTHING;
|
|
|
|
-- Migriere training_style_id → exercise_styles (als primäre Zuordnung)
|
|
INSERT INTO exercise_styles (exercise_id, training_style_id, is_primary)
|
|
SELECT id, training_style_id, true
|
|
FROM exercises
|
|
WHERE training_style_id IS NOT NULL
|
|
ON CONFLICT (exercise_id, training_style_id) DO NOTHING;
|
|
|
|
-- ============================================================================
|
|
-- PHASE 4: Basis-Daten für Zielgruppen
|
|
-- ============================================================================
|
|
|
|
-- Beispiel-Zielgruppen für Shotokan (training_style_id = 1, falls vorhanden)
|
|
DO $$
|
|
DECLARE
|
|
shotokan_id INT;
|
|
BEGIN
|
|
SELECT id INTO shotokan_id FROM training_styles WHERE name = 'Shotokan' LIMIT 1;
|
|
|
|
IF shotokan_id IS NOT NULL THEN
|
|
INSERT INTO target_groups (training_style_id, name, description, sort_order) VALUES
|
|
(shotokan_id, 'Breitensportler', 'Karate für Freizeit und Fitness', 1),
|
|
(shotokan_id, 'Leistungssportler', 'Wettkampforientiertes Training', 2),
|
|
(shotokan_id, 'Kinder', 'Karate für Kinder (6-12 Jahre)', 3)
|
|
ON CONFLICT DO NOTHING;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================================================
|
|
-- PHASE 5: Hierarchie-Daten (Stile zu Fokusbereichen zuordnen)
|
|
-- ============================================================================
|
|
|
|
-- Ordne Karate-Stile dem Fokusbereich "Karate" zu
|
|
DO $$
|
|
DECLARE
|
|
karate_id INT;
|
|
BEGIN
|
|
SELECT id INTO karate_id FROM focus_areas WHERE name = 'Karate' LIMIT 1;
|
|
|
|
IF karate_id IS NOT NULL THEN
|
|
UPDATE training_styles
|
|
SET focus_area_id = karate_id
|
|
WHERE name IN ('Shotokan', 'Goju-Ryu', 'Wado-Ryu', 'Shito-Ryu', 'Kyokushin')
|
|
AND focus_area_id IS NULL;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================================================
|
|
-- HINWEISE für spätere Migrationen
|
|
-- ============================================================================
|
|
|
|
-- Die folgenden Spalten in exercises können später gedroppt werden:
|
|
-- - focus_area_id (deprecated, durch exercise_focus_areas ersetzt)
|
|
-- - training_style_id (deprecated, durch exercise_styles ersetzt)
|
|
-- - training_character_id (bleibt vorerst, separate Dimension)
|
|
|
|
-- Aktuell bleiben sie zur Rückwärtskompatibilität erhalten.
|
|
-- Neue Übungen sollten NUR über M:N-Tabellen zugeordnet werden.
|
|
-- API-Endpoints sollten enriched data aus M:N-Tabellen liefern.
|