shinkan-jinkendo/backend/migrations/008_mn_exercise_relations.sql
Lars 63b1c09975
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Has been cancelled
feat: Migration 008 - M:N Exercise Relations + Hierarchical Catalogs
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)
2026-04-23 08:46:56 +02:00

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.