SQL od podstaw · część 29

Formularze i raporty oparte na bazie danych

Od filtra w formularzu do czytelnego raportu. Przykład PHP, zasady bezpiecznego zapisu i praktyczna ścieżka tworzenia formularzy oraz raportów w Access.

Część bazodanowa INF.03

Formularz zbiera dane, raport je przedstawia

Formularz może służyć do wprowadzania i zmiany rekordów albo tylko ustalania filtra. Raport ma czytelnie przedstawić wynik: nazwy kolumn, zastosowane warunki, sortowanie i ewentualne sumy. Poprawny SQL to dopiero część zadania — użytkownik musi rozumieć, jakie dane ogląda.

Rozdziel trzy warstwy: reguły danych w bazie, logikę aplikacji i wygląd. Pole required w HTML poprawia wygodę, ale walidację trzeba powtórzyć na serwerze. Ograniczenia SQL chronią dane także przed innymi klientami bazy.

Formularz GET i pełny raport w PHP

Kontynuujemy konfigurację z części 28. Konto raportu potrzebuje SELECT na zamowienia i klienci; samo prawo do produkty nie wystarczy. Administrator nadaje wyłącznie potrzebny zakres w oddzielnej bazie ćwiczeniowej. Plik db.php nadal znajduje się poza katalogiem publicznym.

Zapisz kod jako raport.php, dostosuj prywatną ścieżkę konfiguracji i otwórz przez lokalny serwer PHP. Jest to mały raport na syntetycznych danych. Przy dużych zbiorach dodaj stronicowanie i rozsądne limity.

<?php
declare(strict_types=1);
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
header('Content-Type: text/html; charset=UTF-8');
function h(string $v): string {
    return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$status = $_GET['status'] ?? 'oplacone';
if (!is_string($status) || !in_array($status, ['oplacone', 'nowe'], true)) {
    http_response_code(400);
    exit('Niepoprawny status.');
}
try {
    $cfg = require '/sciezka/poza/htdocs/db.php';
    $db = new mysqli($cfg['host'], $cfg['user'], $cfg['password'],
        $cfg['database'], $cfg['port']);
    $db->set_charset('utf8mb4');
    $stmt = $db->prepare('SELECT z.id, z.data, k.imie
        FROM zamowienia z JOIN klienci k ON k.id = z.klient_id
        WHERE z.status = ? ORDER BY z.data, z.id');
    $stmt->bind_param('s', $status);
    $stmt->execute();
    $stmt->bind_result($id, $data, $imie);
    $rows = [];
    while ($stmt->fetch()) {
        $rows[] = ['id' => $id, 'data' => $data, 'imie' => $imie];
    }
    $stmt->close();
    $db->close();
} catch (Throwable $e) {
    error_log('CN: blad raportu; typ=' . get_class($e));
    http_response_code(500);
    exit('Nie udało się przygotować raportu.');
}
?>
<!doctype html>
<html lang="pl">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Raport zamówień</title>
<style>
body{font-family:system-ui;max-width:60rem;margin:2rem auto;padding:1rem}
table{border-collapse:collapse;width:100%}th,td{border:1px solid #aaa;padding:.6rem;text-align:left}
@media print{form{display:none}body{margin:0}}
</style>
<h1>Raport zamówień</h1>
<form method="get">
<label for="status">Status:</label>
<select id="status" name="status">
<option value="oplacone" <?= $status === 'oplacone' ? 'selected' : '' ?>>Opłacone</option>
<option value="nowe" <?= $status === 'nowe' ? 'selected' : '' ?>>Nowe</option>
</select>
<button type="submit">Pokaż raport</button>
</form>
<p>Filtr: <?= h($status) ?>. Liczba zamówień: <?= count($rows) ?>.</p>
<table>
<caption>Zamówienia zgodne z filtrem</caption>
<thead><tr><th scope="col">ID</th><th scope="col">Data</th><th scope="col">Klient</th></tr></thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr><td><?= h((string)$row['id']) ?></td><td><?= h($row['data']) ?></td><td><?= h($row['imie']) ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$rows): ?><p>Brak zamówień dla wybranego filtra.</p><?php endif; ?>
</html>

Na początkowej bazie filtr oplacone, daje zamówienia 1 i 3, oba klientki Ali. Filtr nowe daje 2 i 4. GET nadaje się do odczytu i pozwala zachować adres z filtrem. Lista opcji w HTML nie wystarcza jako zabezpieczenie: kod sprawdza status jeszcze raz na serwerze.

W tabeli zastosowano podpis i nagłówki, a filtr pozostaje widoczny w treści raportu również na wydruku. Styl drukowania ukrywa tylko formularz. Puste wyniki otrzymują komunikat zamiast mylącego błędu. Oczekiwane wyniki opisano dla niezmienionej bazy; wykonanie strony sprawdź we własnym środowisku.

Raport zbiorczy: policz właściwy poziom danych

SELECT k.id, k.imie, COUNT(z.id) AS liczba_zamowien
FROM klienci k
LEFT JOIN zamowienia z ON z.klient_id = k.id
GROUP BY k.id, k.imie
ORDER BY k.id;

To raport obejmujący również klientów bez zamówień. Wyniki dla identyfikatorów 1–5 wynoszą 2, 1, 1, 0, 0. Nie używaj tutaj COUNT(*), ponieważ wiersz klienta bez dopasowania również byłby liczony. Grupowanie po id i imieniu nie scala różnych osób o takim samym imieniu.

Zapytanie ma tę samą podstawową składnię w MySQL/MariaDB, SQL Server, Oracle 19c i Access. Przy raportach kwotowych po dołączeniu pozycji zamówienia powstaje więcej wierszy: licz zamówienia na odpowiednim poziomie, a nie pozycje pod etykietą „liczba zamówień”. Suma kursowego modelu używa bieżącej ceny produktu; prawdziwy system powinien zapisywać cenę z chwili zakupu w pozycji zamówienia.

Wypróbuj zapytanie

SQLite — rzeczywisty silnik przeglądarki. MySQL / SQL Server — ograniczona emulacja składni. Wybierz tryb SQLite do natywnego LIKE. Pozostałe tryby nie są rzeczywistymi serwerami. Ścisłe typy, auto-ID i administrację ćwicz w prawdziwym SZBD.

Ładowanie edytora…

Formularz zapisujący: co musi zrobić serwer?

Dla dodania lub zmiany danych stosuj POST. Sam POST nie chroni przed nieuprawnionym zapisem. Kompletny proces obejmuje uwierzytelnienie, sprawdzenie prawa użytkownika do operacji, walidację, ochronę CSRF, zapytanie parametryzowane, obsługę błędów i potwierdzenie.

  1. Przy wyświetlaniu formularza utwórz losowy token CSRF i zachowaj go w sesji. Wstaw token do ukrytego pola po zakodowaniu dla HTML.
  2. Po POST sprawdź metodę, sesję, uprawnienia i token. Porównuj znaną wartość z serwera z przekazaną wartością, np. przez hash_equals; najpierw sprawdź, czy obie są tekstem.
  3. Waliduj każde pole: dodatnie id, niepuste imię, długości, poprawny e-mail. Świadomie zamieniaj brak wartości na NULL, jeśli schema na to pozwala.
  4. Wykonaj przygotowany INSERT/UPDATE. Nie ujawniaj szczegółów błędów; osobno obsłuż np. zajęty identyfikator lub e-mail.
  5. Po sukcesie zastosuj przekierowanie 303 do strony potwierdzenia, aby zwykłe odświeżenie nie wysyłało formularza ponownie.

Fragment warstwy zapisu, wykonywany dopiero po powyższych kontrolach:

$stmt = $db->prepare(
    'INSERT INTO klienci (id, imie, miasto, email) VALUES (?, ?, ?, ?)'
);
$stmt->bind_param('isss', $id, $imie, $miasto, $email);
$stmt->execute();

W pakiecie kursu id wpisujemy jawnie — nie ma automatycznego numerowania klientów. Fragment nie jest kompletnym publicznym endpointem. Przy rzeczywistej aplikacji potrzebne są także bezpieczne ustawienia sesji, HTTPS, polityka dostępu i obsługa współbieżnych zmian. Dane połączenia nigdy nie trafiają do formularza.

Formularz i raport w Access

  1. W nowym pliku ćwiczeniowym przygotuj tabele i relacje z części 26. Wybierz tabelę klienci i kreator formularzy. Dodaj pola id, imie, miasto, email; ustaw czytelne etykiety i kolejność przechodzenia klawiszem Tab.
  2. Źródłem rekordów formularza może być tabela lub aktualizowalna kwerenda. Raport agregujący zwykle nie nadaje się do bezpośredniego wpisywania pojedynczych rekordów.
  3. W formularzu zamówienia użyj pola kombi, które pokazuje opis klienta, ale zapisuje jego id jako klient_id. Wybór opisu nie powinien zapisywać nazwiska w kolumnie klucza.
  4. Przy formularzu głównym klientów i podformularzu zamówień ustaw powiązanie pól id → klient_id. Sprawdź, czy zmiana klienta pokazuje jego zamówienia.
  5. Zapisz kwerendę raportu zbiorczego jako q_zamowienia_klientow. W kreatorze raportów wybierz ją jako źródło, dodaj pola, sortowanie i czytelny tytuł. Sprawdź podgląd wydruku oraz szerokość kolumn.

SQL Server, Oracle i MySQL są silnikami serwerowymi: formularz i układ wydruku zwykle tworzy aplikacja lub narzędzie raportowe. Access łączy silnik plikowy z narzędziami interfejsu i może też korzystać z tabel połączonych z serwerem. „Lokalny” opisuje miejsce uruchomienia, nie wyłącznie rodzaj silnika.

Ćwiczenia z rozwiązaniami

1. Dlaczego suma klientów z raportu wynosi 4 zamówienia, mimo że klientów jest 5?

Ala ma 2, Jan 1, O’Neil 1, Ewa i Adam po 0. COUNT(z.id) z LEFT JOIN zachowuje klientów bez zamówień i nie liczy pustego dopasowania jako zamówienia.

2. Zaprojektuj formularz zmiany miasta klienta

Potrzebujesz identyfikatora klienta oraz miasta, walidacji, kontroli prawa do tego rekordu, POST i CSRF. SQL: UPDATE klienci SET miasto = ? WHERE id = ?. Parametry s i i. Nie przyjmuj fragmentu WHERE od użytkownika; brak zgody na rekord nie może być zastąpiony samą znajomością id.

3. Zbuduj raport i przygotuj wydruk

W PHP albo Access pokaż pięciu klientów i liczbę ich zamówień. Dodaj opis źródła i datę przygotowania, sprawdź etykiety, zera i sortowanie. Wydruk/PDF nie powinien obcinać kolumn. Zachowaj własny autentyczny zrzut wyniku.

Źródła

Access: tworzenie raportu · Access: kwerenda jako źródło rekordów · PHP: hash_equals

Przewijanie do góry