student_id-FK-Migration Stufe 1+2: Name-Joins -> student_sessions.student_id

- student_sessions: neue Spalte student_id (+Index), backfilled aus (class_id,display_name)
- Session::createStudent setzt student_id (per Lookup, falls nicht uebergeben)
- alle Joins students<->student_sessions von display_name auf student_id umgestellt:
  Benchmark.php, live.php (5x), results.php (4x + runs), modules.php, profile.php, progress.php (answers+runs)
- verlustfrei verifiziert (alt-Join == neu-Join, class 99/102)

Robuster gegen Umbenennung/Doppelnamen, indexierbare Joins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 19:16:41 +02:00
parent 2984d93e7e
commit fb44ec7afe
7 changed files with 42 additions and 30 deletions
+8 -8
View File
@@ -93,12 +93,12 @@ if ($method === 'GET') {
s.id AS student_id, s.display_name, s.username, s.emoji_avatar, s.avatar_slug
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ? AND s.display_name = ss.display_name
JOIN students s ON s.id = ss.student_id
WHERE a.class_id = ?
AND a.submitted_at >= ?
AND a.submitted_at < ?
ORDER BY s.id, a.sim_id, a.submitted_at ASC",
[$classId, $classId, $date . ' 00:00:00', $date . ' 23:59:59.999']
[$classId, $date . ' 00:00:00', $date . ' 23:59:59.999']
);
// Helper: extrahiere "Lektion-Key" (mission/level/scenario) aus assessments-row
@@ -394,10 +394,10 @@ if ($method === 'GET') {
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
WHERE a.class_id = ? AND a.sim_id = ?
AND ss.display_name = ?
AND ss.student_id = ?
AND a.submitted_at BETWEEN ? AND ?
ORDER BY a.submitted_at DESC',
[$classId, $simId, $student['display_name'], $start, $end]
[$classId, $simId, $studentId, $start, $end]
);
if (!$rows) Response::ok(['results' => null, 'assessmentCount' => 0]);
// Nimm den Row mit dem reichhaltigsten Results-JSON (Heuristik: hat .stars oder .results.totalStars)
@@ -600,7 +600,7 @@ if ($method === 'GET') {
$heuteRows = $db->fetchAll(
"SELECT DISTINCT s.id AS student_id
FROM students s
LEFT JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name
LEFT JOIN student_sessions ss ON ss.student_id = s.id
LEFT JOIN assessments a ON a.session_id = ss.id AND a.submitted_at >= CURDATE()
LEFT JOIN live_sessions ls ON ls.student_id = s.id AND ls.last_seen >= CURDATE()
WHERE s.class_id = ? AND s.deleted_at IS NULL
@@ -616,7 +616,7 @@ if ($method === 'GET') {
MAX(GREATEST(IFNULL(a.submitted_at, '1970-01-01'),
IFNULL(ls.last_seen, '1970-01-01'))) AS last_seen_any
FROM students s
LEFT JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name
LEFT JOIN student_sessions ss ON ss.student_id = s.id
LEFT JOIN assessments a ON a.session_id = ss.id
LEFT JOIN live_sessions ls ON ls.student_id = s.id
WHERE s.class_id = ? AND s.deleted_at IS NULL
@@ -633,7 +633,7 @@ if ($method === 'GET') {
"SELECT s.id AS student_id, a.sim_id, a.results, a.process_log, a.submitted_at
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name
JOIN students s ON s.id = ss.student_id
WHERE a.class_id = ? AND s.deleted_at IS NULL
ORDER BY s.id, a.sim_id, a.submitted_at DESC",
[$classId]
@@ -729,7 +729,7 @@ if ($method === 'GET') {
SUM(COALESCE(a.duration_ms, 0)) / 1000 AS sec
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name
JOIN students s ON s.id = ss.student_id
WHERE a.class_id = ? AND $sinceSql AND s.deleted_at IS NULL
GROUP BY bucket, a.sim_id",
[$classId]
+1 -1
View File
@@ -74,7 +74,7 @@ if ($method === 'GET') {
// Individuelle Overrides für diesen Schüler
$studentRow = $db->fetchOne(
'SELECT s.id FROM students s JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name WHERE ss.id = ?',
'SELECT student_id AS id FROM student_sessions WHERE id = ?',
[$sessionId]
);
$overrides = [];
+10 -6
View File
@@ -16,21 +16,25 @@ if ($method === 'GET') {
// ─── Sub-View: eigene Reflexionen einer Schüler:in ──────────────────
if (($_GET['view'] ?? '') === 'reflections') {
if (!$studentSessionId) Response::error('Nur eingeloggte Schüler:innen', 401);
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$studentSessionId]);
$session = $db->fetchOne('SELECT class_id, display_name, student_id FROM student_sessions WHERE id = ?', [$studentSessionId]);
if (!$session) Response::error('Session nicht gefunden', 404);
// Reflexionen über alle assessments dieser Schüler:in (matched über
// class_id + display_name, weil verschiedene Sessions denselben
// Bearbeiter:innen-Namen tragen können).
// student_id (FK) statt fragilem display_name-Join; Fallback für seltene Alt-Sessions ohne FK.
$sid = $session['student_id'] ?? null;
if (!$sid) {
$st = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]);
$sid = $st ? (int)$st['id'] : 0;
}
// Reflexionen über alle Sessions dieser Schüler:in (per student_id verknüpft).
$rows = $db->fetchAll(
"SELECT a.sim_id, a.submitted_at, a.reflections
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
WHERE ss.class_id = ? AND ss.display_name = ?
WHERE ss.student_id = ?
AND a.reflections IS NOT NULL
AND JSON_LENGTH(a.reflections) > 0
ORDER BY a.submitted_at DESC
LIMIT 50",
[$session['class_id'], $session['display_name']]
[$sid]
);
$items = [];
foreach ($rows as $r) {
+3 -3
View File
@@ -44,7 +44,7 @@ if ($method === 'GET') {
if ($studentId && $simId) {
$progress = $db->fetchOne('SELECT level, xp, plays, best_stars FROM player_progress WHERE student_id = ? AND sim_id = ?', [$studentId, $simId]);
$answers = $db->fetchAll('SELECT phase, question_id, answer, correct, answered_at FROM assessment_answers WHERE session_id IN (SELECT id FROM student_sessions WHERE class_id = (SELECT class_id FROM students WHERE id = ?)) AND sim_id = ? ORDER BY answered_at DESC', [$studentId, $simId]);
$answers = $db->fetchAll('SELECT phase, question_id, answer, correct, answered_at FROM assessment_answers WHERE session_id IN (SELECT id FROM student_sessions WHERE student_id = ?) AND sim_id = ? ORDER BY answered_at DESC', [$studentId, $simId]);
// Optional: alle Durchgänge mit results-JSON, wenn ?runs=1 → für Schüler-Eigen-Drilldown
$runs = [];
if (!empty($_GET['runs'])) {
@@ -59,7 +59,7 @@ if ($method === 'GET') {
"SELECT a.id, a.submitted_at, a.duration_ms, a.results
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
WHERE ss.class_id = ? AND ss.display_name = ? AND a.sim_id = ?
WHERE ss.student_id = ? AND a.sim_id = ?
AND a.results IS NOT NULL
AND (
JSON_EXTRACT(a.results, '$.stars') IS NOT NULL
@@ -70,7 +70,7 @@ if ($method === 'GET') {
)
ORDER BY a.submitted_at DESC
LIMIT 50",
[$student['class_id'], $student['display_name'], $simId]
[$studentId, $simId]
);
foreach ($rows as $row) {
$row['results'] = $row['results'] ? json_decode($row['results'], true) : null;
+6 -6
View File
@@ -206,7 +206,7 @@ if ($view === 'matrix') {
"SELECT s.id AS student_id, a.sim_id, a.results, a.process_log
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name
JOIN students s ON s.id = ss.student_id
WHERE a.class_id = ? AND s.deleted_at IS NULL
ORDER BY s.id, a.sim_id, a.submitted_at DESC",
[$classId]
@@ -387,7 +387,7 @@ if ($view === 'fleiss') {
COUNT(*) AS sessions
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name
JOIN students s ON s.id = ss.student_id
WHERE a.class_id = ? AND $sinceSql AND s.deleted_at IS NULL $modSql
GROUP BY s.id, bucket, a.sim_id",
$params
@@ -553,7 +553,7 @@ if ($view === 'activity') {
"SELECT $bucketA AS bucket, s.id AS student_id, s.display_name AS name, s.emoji_avatar AS emoji, COUNT(*) AS n
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name
JOIN students s ON s.id = ss.student_id
WHERE a.class_id = ? AND $sinceSqlA AND s.deleted_at IS NULL
GROUP BY bucket, s.id",
[$classId]
@@ -656,10 +656,10 @@ if ($view === 'module') {
"SELECT a.id, a.submitted_at, a.duration_ms, a.results, a.reflections
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
WHERE ss.class_id = ? AND ss.display_name = ? AND a.sim_id = ?
WHERE ss.student_id = ? AND a.sim_id = ?
ORDER BY a.submitted_at DESC
LIMIT 100",
[$classId, $student['display_name'], $moduleId]
[$studentId, $moduleId]
);
// results + reflections JSON parsen (Frontend sieht's lieber dekodiert)
foreach ($runs as &$run) {
@@ -851,7 +851,7 @@ if ($view === 'reflections') {
ss.display_name, s.id AS student_id, s.emoji_avatar
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
LEFT JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name AND s.deleted_at IS NULL
LEFT JOIN students s ON s.id = ss.student_id AND s.deleted_at IS NULL
WHERE a.class_id = ?
AND a.reflections IS NOT NULL
AND JSON_LENGTH(a.reflections) > 0";
+1 -2
View File
@@ -76,8 +76,7 @@ class Benchmark
"SELECT a.sim_id, a.results, a.process_log, a.submitted_at
FROM assessments a
JOIN student_sessions ss ON ss.id = a.session_id
JOIN students s ON s.class_id = ss.class_id AND s.display_name = ss.display_name
WHERE s.id = ? AND a.class_id = ?
WHERE ss.student_id = ? AND a.class_id = ?
ORDER BY a.sim_id, a.submitted_at DESC",
[$studentId, $classId]
);
+13 -4
View File
@@ -20,8 +20,10 @@ class Session {
return $_COOKIE[self::COOKIE_NAME] ?? null;
}
/** Neue Schueler-Session erstellen */
public static function createStudent(int $classId, string $displayName = ''): string {
/** Neue Schueler-Session erstellen. student_id wird direkt gesetzt (per FK
* statt fragilem display_name-Join) — falls nicht übergeben, aus
* (class_id, display_name) aufgelöst. */
public static function createStudent(int $classId, string $displayName = '', ?int $studentId = null): string {
$uuid = sprintf('%s-%s-%s-%s-%s',
bin2hex(random_bytes(4)),
bin2hex(random_bytes(2)),
@@ -31,9 +33,16 @@ class Session {
);
$db = Database::get();
if ($studentId === null && $displayName !== '') {
$row = $db->fetchOne(
'SELECT id FROM students WHERE class_id = ? AND display_name = ? AND deleted_at IS NULL',
[$classId, $displayName]
);
if ($row) $studentId = (int)$row['id'];
}
$db->execute(
'INSERT INTO student_sessions (id, class_id, display_name) VALUES (?, ?, ?)',
[$uuid, $classId, $displayName]
'INSERT INTO student_sessions (id, class_id, display_name, student_id) VALUES (?, ?, ?, ?)',
[$uuid, $classId, $displayName, $studentId]
);
setcookie(self::COOKIE_NAME, $uuid, [