1. 🧱 Struktura Bazy Danych (DDL) - Finalna Wersja
Zaktualizowano tabelę **`gracze`** o kolumny **`ranking`**, **`data_ostatniego_punkty`**, **`username`** i **`password`** do zaawansowanych ćwiczeń.
| Tabela | Dodane/Zmienione Kolumny | Opis |
|---|---|---|
gracze | **ranking** (INT), **data_ostatniego_punkty** (DATE), **username** (VARCHAR), **password** (VARCHAR) | Niezbędne dla Zad. 2 i 8. |
mecze | id_gracza, id_druzyny |
Skrypt Inicjalizacyjny (Tylko SQL)
Pokaż Skrypt Tworzenia Bazy (Schema + Przykładowe Dane)
-- DROP i CREATE TABLE dla czystego schematu (Wymagany do zaawansowanych ćwiczeń)
-- Tabela Gracze z nowymi kolumnami
CREATE TABLE gracze (
id INT PRIMARY KEY AUTO_INCREMENT,
nazwa VARCHAR(100) NOT NULL,
nick VARCHAR(50) UNIQUE,
punkty INT DEFAULT 0,
id_druzyna INT,
ranking INT, -- Nowa kolumna dla Zadania 2
data_ostatniego_punkty DATE, -- Nowa kolumna dla Zadania 8
username VARCHAR(50) UNIQUE, -- Nowa kolumna dla Zadania 10
password VARCHAR(255) -- Nowa kolumna dla Zadania 10
);
-- Tabela Mecze z kluczem id_gracza
CREATE TABLE mecze (
id INT PRIMARY KEY AUTO_INCREMENT,
id_druzyny INT NOT NULL,
id_gracza INT,
wynik VARCHAR(50),
wygrana BOOLEAN,
FOREIGN KEY (id_gracza) REFERENCES gracze(id)
);
-- Przykładowe dane
INSERT INTO gracze (nazwa, nick, punkty, username, password, data_ostatniego_punkty) VALUES
('Jan Kowalski', 'Kowalski', 90, 'janek', 'haslo123', CURDATE()), -- Punkty < 100 dla testu transakcji
('Piotr Nowak', 'Piotrek', 1500, 'piotrek', 'bezpieczne', DATE_SUB(CURDATE(), INTERVAL 60 DAY)),
('Anna Lewa', 'Lewa_A', 900, 'anna', 'qwerty', CURDATE());
INSERT INTO mecze (id_gracza, wygrana) VALUES
(1, 1), (1, 0), (2, 1); -- Przykładowe mecze
2. 🚀 Programowanie SQL (Zaawansowane)
Ćwicz tworzenie logiki zorientowanej na bazie danych.
2.1. Transakcja aktualizująca punkty (TRANSACTION)
Zadanie: Napisz transakcję, która zwiększy punkty gracza o **10** tylko wtedy, gdy jego aktualna liczba punktów jest **mniejsza niż 100**.
Rozwiązanie
START TRANSACTION;
-- Użyj konkretnego ID do testu, np. ID=1 (Kowalski, 90 pkt)
UPDATE gracze
SET punkty = punkty + 10
WHERE id = 1 AND punkty < 100;
-- Sprawdzenie, czy aktualizacja się powiodła
SELECT punkty FROM gracze WHERE id = 1;
COMMIT; -- Zatwierdzenie zmian
-- ROLLBACK; -- Alternatywnie, wycofanie zmian
2.2. Złożona procedura rankingowa z Kursorem (PROCEDURE + CURSOR)
Zadanie: Utwórz procedurę aktualizuj_ranking, która przechodzi przez wszystkich graczy i ustawia ich **`ranking`** (kolumna z DDL) na podstawie punktów. Użyj kursora.
Rozwiązanie
DELIMITER //
CREATE PROCEDURE aktualizuj_ranking()
BEGIN
DECLARE koniec INT DEFAULT 0;
DECLARE id_gracza INT;
-- Definicja Kursora
DECLARE cur CURSOR FOR SELECT id FROM gracze ORDER BY punkty DESC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET koniec = 1;
SET @ranking = 1;
OPEN cur;
petla: LOOP
FETCH cur INTO id_gracza;
IF koniec THEN
LEAVE petla;
END IF;
-- Aktualizacja rankingu
UPDATE gracze SET ranking = @ranking WHERE id = id_gracza;
SET @ranking = @ranking + 1;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
-- Wywołanie: CALL aktualizuj_ranking();
2.3. Wyzwalacz blokujący zbyt wiele rozegranych meczów (TRIGGER + SIGNAL)
Zadanie: Utwórz wyzwalacz, który blokuje dodanie meczu, jeśli dany gracz rozegrał już **100 lub więcej** meczów (walidacja biznesowa).
Rozwiązanie
DELIMITER //
CREATE TRIGGER blokada_meczy
BEFORE INSERT ON mecze
FOR EACH ROW
BEGIN
DECLARE rozegrane INT;
SELECT COUNT(*) INTO rozegrane
FROM mecze
WHERE id_gracza = NEW.id_gracza;
IF rozegrane >= 100 THEN
-- Wygenerowanie błędu, który przerwie INSERT
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Gracz osiągnął limit 100 meczów.';
END IF;
END //
DELIMITER ;
2.4. Funkcja zwracająca liczbę meczów (UDF)
Zadanie: Napisz **funkcję**, która przyjmuje **`id_gracza`** i zwraca liczbę rozegranych przez niego meczów.
Rozwiązanie
DELIMITER //
CREATE FUNCTION liczba_meczy(gracz_id INT)
RETURNS INT
DETERMINISTIC
BEGIN
DECLARE liczba INT;
SELECT COUNT(*) INTO liczba FROM mecze WHERE id_gracza = gracz_id;
RETURN liczba;
END //
DELIMITER ;
-- Wywołanie: SELECT liczba_meczy(1);
3. 📊 Optymalizacja i Zarządzanie
Ćwicz zagadnienia związane z wydajnością i zarządzaniem DDL/DML.
3.1. Optymalizacja zapytania rankingowego (INDEX)
Zadanie: Utwórz **indeks** na kolumnie punkty w tabeli gracze dla szybszych zapytań rankingowych (sortowanie malejąco).
Rozwiązanie
CREATE INDEX idx_punkty_desc ON gracze(punkty DESC);
3.2. Usunięcie gracza i powiązanych danych (CASCADING DELETE)
Zadanie: Usuń gracza o ID=1 z tabeli gracze oraz **wszystkie jego mecze**. (Najbezpieczniej jest najpierw usunąć rekordy zależne).
Rozwiązanie
-- 1. Usuń rekordy zależne
DELETE FROM mecze WHERE id_gracza = 1;
-- 2. Usuń gracza
DELETE FROM gracze WHERE id = 1;
-- UWAGA: Aby usunięcie rekordów zależnych było automatyczne,
-- należy dodać ON DELETE CASCADE do definicji klucza obcego przy tworzeniu tabeli.
4. 🌐 Integracja PHP/Bezpieczeństwo
Ćwicz bezpieczne łączenie SQL z kodem aplikacji.
4.1. Dynamiczny raport w PHP z filtrowaniem daty
Zadanie: Napisz skrypt PHP, który generuje raport z rankingiem graczy, filtrując tych, którzy uzyskali punkty w ostatnich **30 dniach** (wykorzystaj kolumnę **`data_ostatniego_punkty`**).
Rozwiązanie
<?php
// Zakładamy, że połączenie PDO jest już skonfigurowane
$pdo = new PDO('mysql:host=localhost;dbname=esport', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$czas = $_GET['czas'] ?? 30; // Domyślnie 30 dni, ale można podmienić
$stmt = $pdo->prepare("
SELECT nazwa, punkty, data_ostatniego_punkty
FROM gracze
WHERE data_ostatniego_punkty >= DATE_SUB(NOW(), INTERVAL :czas DAY)
ORDER BY punkty DESC
");
// Parametryzacja zapytania - BEZPIECZEŃSTWO
$stmt->bindParam(':czas', $czas, PDO::PARAM_INT);
$stmt->execute();
echo '<h2>Raport Rankingowy (Aktywni)';
echo '<table>';
echo '<tr><th>Nazwa<th>Punkty<th>Ostatni Punkt';
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo '<tr>';
echo '<td>' . htmlspecialchars($row['nazwa']) . '';
echo '<td>' . htmlspecialchars($row['punkty']) . '';
echo '<td>' . htmlspecialchars($row['data_ostatniego_punkty']) . '';
echo '</tr>';
}
echo '</table>';
?>
4.2. Bezpieczeństwo (SQL Injection) w PHP
Zadanie: Zabezpiecz fragment kodu logowania przed SQL Injection, stosując **przygotowane zapytania (Prepared Statements)** w PHP.
Rozwiązanie
<?php
// Zabezpieczony kod logowania z Prepared Statements
$pdo = new PDO('mysql:host=localhost;dbname=esport', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
// Użycie placeholderów (?) lub nazwanego placeholdera (:nazwa)
$stmt = $pdo->prepare('SELECT id FROM gracze WHERE username = :username AND password = :password');
// Właściwe zabezpieczenie: Wartości są przekazywane ODDZIELNIE od zapytania.
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
$stmt->execute();
if ($stmt->rowCount() > 0) {
echo 'Zalogowano pomyślnie!';
} else {
echo 'Błędne dane logowania.';
}
?>
4.3. Złożone złączenia (JOIN) - Podsumowanie
Zadanie: Napisz zapytanie, które połączy tabelę gracze z tabelą mecze i wyświetli nazwiska graczy oraz liczbę rozegranych meczów.
Rozwiązanie
SELECT g.nazwa, COUNT(m.id) AS liczba_meczy
FROM gracze g
LEFT JOIN mecze m ON g.id = m.id_gracza
GROUP BY g.nazwa;