Szybki quiz – przypomnienie poprzedniej lekcji
Lekcja 19: INSERT, UPDATE, DELETE z PHP.
1. Ola ma id = 2 i dokładnie 100 XP. Co wypisze ten kod?
$xp = 100;
$id = 2;
$zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET xp = ? WHERE id = ?');
$zapytanie->bind_param('ii', $xp, $id);
$zapytanie->execute();
var_dump($zapytanie->affected_rows);
Uzasadnij wynik.
2. Znajdź błąd:
$zapytanie = $polaczenie->prepare('INSERT INTO zadania (uzytkownik_id, tytul) VALUES (?, ?)');
$zapytanie->bind_param('is', 2, $tytul);
3. Dlaczego ostatnia linia nie ma sensu?
$zapytanie = $polaczenie->prepare('DELETE FROM zadania WHERE id = ?');
$zapytanie->bind_param('i', $zadanieId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
4. Uzupełnij warunek tak, aby XP za zadanie można było dostać tylko raz i tylko za swoje zadanie:
UPDATE zadania SET data_wykonania = CURDATE()
WHERE ...
5. Skrypt dodający zadanie wykonuje INSERT, a potem od razu wypisuje HTML z komunikatem „Dodano”. Co się stanie, gdy użytkownik wciśnie F5? Jak nazywa się wzorzec, który rozwiązuje ten problem, i z jakich trzech kroków się składa?
6. Co zwraca $zapytanie->insert_id i która funkcja z czasów CSV stała się dzięki temu niepotrzebna?
7. W DELETE FROM zadania pominięto WHERE. Ile wierszy zniknie i co zwróci affected_rows?
Odpowiedzi
1. int(0). Zapytanie wykonało się poprawnie, wiersz pasował do WHERE, ale wartość się nie zmieniła (było 100, wpisujemy 100). affected_rows przy UPDATE liczy wiersze faktycznie zmienione, a nie pasujące do warunku.
2. 2 to stała, a bind_param() przyjmuje wyłącznie zmienne (przekazuje je przez referencję) — będzie Error: ... could not be passed by reference. Poprawnie: $uzytkownikId = 2; i bind_param('is', $uzytkownikId, $tytul);.
3. DELETE nie zwraca wierszy, więc nie ma czego odbierać przez get_result(). Efekt sprawdzamy przez $zapytanie->affected_rows.
4. WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL — pierwszy warunek wskazuje zadanie, drugi pilnuje właściciela (id z formularza można podrobić, $_SESSION['uzytkownik_id'] nie), trzeci sprawia, że drugie kliknięcie zmieni 0 wierszy i XP nie wpadnie dwa razy.
5. F5 powtarza żądanie POST, więc INSERT wykona się ponownie i powstanie duplikat. Rozwiązanie to PRG (Post/Redirect/Get): POST przetwarza dane → header('Location: ...') + exit → przeglądarka pobiera stronę metodą GET, więc odświeżenie nie powtarza zapisu.
6. Zwraca id nadane właśnie dodanemu wierszowi przez AUTO_INCREMENT. Zastąpiło nastepneIdZadania(), które przy CSV musiało samo wyliczać kolejny numer.
7. Zniknie cała zawartość tabeli, a affected_rows pokaże liczbę usuniętych wierszy. WHERE jest jedynym bezpiecznikiem — baza nie zapyta „na pewno?”.
PDO (PHP Data Objects) – ZALECANE dla większości projektów
PDO to obecnie najlepszy wybór dla nowoczesnych aplikacji PHP ze względu na:
- Uniwersalność (możliwość zmiany bazy danych)
- Czytelność kodu (named parameters)
- Bezpieczeństwo (domyślne prepared statements)
- Zgodność ze standardami branżowymi
- Wsparcie dla frameworków (Laravel, Symfony używają PDO)
Szczegółowe porównanie tabelaryczne
| Kryterium | PDO | MySQLi (obiektowo) | MySQLi (proceduralnie) |
|---|---|---|---|
| Obsługiwane bazy danych | MySQL, PostgreSQL, SQLite, Oracle, MS SQL Server | Tylko MySQL/MariaDB | Tylko MySQL/MariaDB |
| Styl programowania | Tylko obiektowy (nowoczesny) | Obiektowy | Proceduralny (przestarzały) |
| Prepared statements | Named + positional | Tylko positional | Tylko positional |
| Named parameters | :email, :id (czytelne) | Tylko ? | Tylko ? |
| Obsługa błędów | Exceptions (nowoczesne) | Exceptions + kody | Głównie kody błędów |
| Transakcje | beginTransaction(), commit(), rollback() | begin_transaction(), commit(), rollback() | mysqli_begin_transaction() |
| Wydajność | Bardzo dobra | Nieznacznie szybsza (1-2%) | Nieznacznie szybsza |
| Czytelność kodu | Bardzo wysoka | Wysoka | Niska |
| Popularność w 2024 | 60% projektów | 35% projektów | 5% projektów (legacy) |
| Frameworki | Laravel, Symfony, Slim, Yii | Rzadko używane | Nie używane |
| Wsparcie społeczności | Bardzo duże | Duże | Małe |
| Łatwość nauki | Bardzo łatwe | Łatwe | Łatwe ale przestarzałe |
| Przenośność kodu | Wysoka (zmiana bazy bez przepisywania) | Niska (tylko MySQL) | Niska |
| Zgodność z OOP | W pełni obiektowe | Obiektowe | Nie obiektowe |
| Multiple queries | Nie wspiera (bezpieczniejsze) | Wspiera | Wspiera |
| Asynchroniczne zapytania | Nie wspiera | Wspiera | Wspiera |
| Polecane dla | Nowe projekty, aplikacje komercyjne | Projekty tylko MySQL | Stare projekty (nie polecane) |
Porównanie kodu – Przykłady praktyczne
Przykład 1: Połączenie z bazą
PDO (zalecane):
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=app_db;charset=utf8mb4',
'username',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
echo "Połączono z bazą!";
} catch (PDOException $e) {
die("Błąd: " . $e->getMessage());
}
MySQLi (obiektowo):
$mysqli = new mysqli('localhost', 'username', 'password', 'app_db');
if ($mysqli->connect_error) {
die("Błąd: " . $mysqli->connect_error);
}
$mysqli->set_charset('utf8mb4');
echo "Połączono z bazą!";
MySQLi (proceduralnie) – NIE POLECANE:
$conn = mysqli_connect('localhost', 'username', 'password', 'app_db');
if (!$conn) {
die("Błąd: " . mysqli_connect_error());
}
mysqli_set_charset($conn, 'utf8mb4');
echo "Połączono z bazą!";
Werdykt: PDO wygrywa – zwięzłe, z automatyczną obsługą wyjątków.
Przykład 2: Prepared Statements (bezpieczeństwo)
PDO (zalecane) – Named Parameters:
// Bardzo czytelne - wiadomo co jest czym
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND active = :active");
$stmt->execute([
'email' => $email,
'active' => 1
]);
$user = $stmt->fetch();
MySQLi (obiektowo):
// Mniej czytelne - trzeba pamiętać kolejność
$stmt = $mysqli->prepare("SELECT * FROM users WHERE email = ? AND active = ?");
$aktywny = 1;
$stmt->bind_param("si", $email, $aktywny); // s=string, i=integer
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
MySQLi (proceduralnie) – NIE POLECANE:
// Najbardziej rozwlekłe
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE email = ? AND active = ?");
$aktywny = 1;
mysqli_stmt_bind_param($stmt, "si", $email, $aktywny);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
Przykład 3: INSERT z pobieraniem ID
PDO (zalecane):
$stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (:username, :email)");
$stmt->execute([
'username' => $username,
'email' => $email
]);
$userId = $pdo->lastInsertId();
echo "Dodano użytkownika ID: $userId";
MySQLi (obiektowo):
$stmt = $mysqli->prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $email);
$stmt->execute();
$userId = $mysqli->insert_id;
echo "Dodano użytkownika ID: $userId";
MySQLi (proceduralnie) – NIE POLECANE:
$stmt = mysqli_prepare($conn, "INSERT INTO users (username, email) VALUES (?, ?)");
mysqli_stmt_bind_param($stmt, "ss", $username, $email);
mysqli_stmt_execute($stmt);
$userId = mysqli_insert_id($conn);
echo "Dodano użytkownika ID: $userId";
Przykład 4: Transakcje
PDO (zalecane):
try {
$pdo->beginTransaction();
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$pdo->commit();
echo "Przelew wykonany!";
} catch (Exception $e) {
$pdo->rollBack();
echo "Błąd: " . $e->getMessage();
}
MySQLi (obiektowo):
$mysqli->begin_transaction();
try {
$mysqli->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$mysqli->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$mysqli->commit();
echo "Przelew wykonany!";
} catch (Exception $e) {
$mysqli->rollback();
echo "Błąd: " . $e->getMessage();
}
MySQLi (proceduralnie) – NIE POLECANE:
mysqli_begin_transaction($conn);
$success = mysqli_query($conn, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$success = $success && mysqli_query($conn, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");
if ($success) {
mysqli_commit($conn);
echo "Przelew wykonany!";
} else {
mysqli_rollback($conn);
echo "Błąd: " . mysqli_error($conn);
}
Kiedy używać którego rozwiązania?
- Budujesz nowy projekt (zawsze!)
- Chcesz mieć możliwość zmiany bazy danych w przyszłości
- Pracujesz nad aplikacją komercyjną
- Uczysz się nowoczesnego PHP
- Chcesz kodu zgodnego z branżowymi standardami
- Planujesz używać frameworków (Laravel, Symfony)
- Cenisz czytelność kodu (named parameters)
Przykłady projektów:
- System CRM
- E-commerce
- Blog/CMS
- API REST
- Aplikacja webowa SaaS
- Panel Discord bota
- System zarządzania treningami
Używaj MySQLi (obiektowo) gdy:
- Na 100% będziesz używać tylko MySQL/MariaDB
- Potrzebujesz asynchronicznych zapytań
- Potrzebujesz multiple statements
- Pracujesz z istniejącym projektem używającym MySQLi
- Wydajność jest KRYTYCZNA (różnica ~1-2%)
Przykłady projektów:
- Wysoko obciążony system (miliony zapytań/dzień)
- Integracje wymagające multiple queries
- Migracja starego projektu z mysql do mysqli
Używaj MySQLi (proceduralnie) TYLKO gdy:
- Pracujesz z bardzo starym kodem legacy
- Musisz utrzymywać stary projekt
NIE UŻYWAJ w nowych projektach! To przestarzałe podejście.
PODSUMOWANIE LEKCJI
W skrócie
Ta lekcja nie wprowadza nowej funkcjonalności — porządkuje wybór narzędzia. Najważniejsze do zrozumienia:
- mysqli i PDO robią to samo: łączą PHP z bazą, wysyłają zapytania, odbierają wyniki. Różnią się zasięgiem i składnią, a nie możliwościami.
- Zasięg: mysqli działa tylko z MySQL/MariaDB. PDO obsługuje wiele baz — zmienia się DSN (
mysql:,pgsql:,sqlite:), ale samo SQL i tak trzeba sprawdzić, bo dialekty się różnią. „Przenośność” nie znaczy „kod zadziała bez zmian”. - Proceduralnie vs obiektowo:
mysqli_query($conn, ...)i$conn->query(...)to ta sama biblioteka w dwóch stylach. Nowego kodu nie piszemy proceduralnie — wersję proceduralną trzeba jednak umieć przeczytać, bo pełno jej w starych poradnikach i przykładach w sieci. - Bezpieczeństwo nie zależy od biblioteki. Zapytanie sklejone ze stringów jest tak samo podatne na SQL injection w PDO, jak w mysqli. Chronią prepared statements, a nie wybór rozszerzenia.
- Obsługa błędów wymaga ustawienia w obu: w PDO
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, w mysqlimysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT). W nowszych wersjach PHP oba zachowują się tak domyślnie, ale piszemy je jawnie — kod nie zależy wtedy od konfiguracji serwera. - Nie mieszamy bibliotek w jednym projekcie. Obiekt PDO nie ma metody
bind_param(), a obiekt mysqli nie mafetchAll(). TaskQuest od Lekcji 17 korzysta z mysqli obiektowo i tak zostaje — rekomendacja PDO nie oznacza, że mysqli jest błędem. - Kryteria realnego wyboru (w tej kolejności): czego używa istniejący kod projektu → czy na pewno tylko MySQL → czego wymaga framework → co zespół zna najlepiej. Wydajność w praktyce nie rozstrzyga.
Typowe źródła błędów przy przesiadce z jednego na drugie:
- w mysqli
bind_param()przyjmuje tylko zmienne (nie stałe, nie wyrażenia) i liczba liter typu musi równać się liczbie?, - w PDO z
ATTR_EMULATE_PREPARES => falsewartość doLIMITtrzeba związać jako liczbę (bindValue(':limit', $limit, PDO::PARAM_INT)), bo domyślnie pójdzie jako tekst i zapytanie się wywali, get_result()istnieje tylko w mysqli; w PDO wynik odbiera się prosto ze$stmt.
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
- mysqli = tylko MySQL/MariaDB. PDO = wiele baz. To jest główna różnica.
- Wersja proceduralna (
mysqli_*) — tylko do czytania starego kodu. Nie piszesz w niej nowych projektów. - Prepared statements są w obu i to one dają bezpieczeństwo, nie sam wybór biblioteki.
- PDO ma parametry nazwane
:email, mysqli tylko?z literami typów (i,d,s,b). - Zamknięcie połączenia:
$polaczenie->close()(mysqli) /$pdo = null(PDO) — na egzaminie wymagane jawnie. - Jeden projekt = jedna biblioteka. Mieszanie kończy się błędem „Call to undefined method”.
- TaskQuest używa mysqli obiektowo — i przy tym zostajemy do końca kursu.
Ściągawka – najważniejsze polecenia i składnia
Te same czynności w obu bibliotekach
| Czynność | mysqli (obiektowo) | PDO |
|---|---|---|
| Połączenie | new mysqli($host, $user, $pass, $baza) |
new PDO("mysql:host=$host;dbname=$baza;charset=utf8mb4", $user, $pass) |
| Kodowanie | $db->set_charset('utf8mb4') |
w DSN: charset=utf8mb4 |
| Błędy jako wyjątki | mysqli_report(MYSQLI_REPORT_ERROR \| MYSQLI_REPORT_STRICT) |
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION |
| Klasa wyjątku | mysqli_sql_exception |
PDOException |
| Przygotowanie | $st = $db->prepare('... WHERE id = ?') |
$st = $pdo->prepare('... WHERE id = :id') |
| Podstawienie danych | $st->bind_param('i', $id) + $st->execute() |
$st->execute(['id' => $id]) |
Wynik SELECT |
$wynik = $st->get_result() |
wprost ze $st |
| Jeden wiersz | $wynik->fetch_assoc() |
$st->fetch() |
| Wszystkie wiersze | $wynik->fetch_all(MYSQLI_ASSOC) |
$st->fetchAll() |
Liczba wierszy SELECT |
$wynik->num_rows |
count($st->fetchAll()) |
| Zmienione wiersze | $st->affected_rows |
$st->rowCount() |
Id po INSERT |
$st->insert_id |
$pdo->lastInsertId() |
| Transakcje | begin_transaction() / commit() / rollback() |
beginTransaction() / commit() / rollBack() |
| Zamknięcie | $db->close() |
$pdo = null; |
Szkielet – mysqli obiektowo
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'nazwa_bazy');
$polaczenie->set_charset('utf8mb4');
$zapytanie = $polaczenie->prepare('SELECT id, nazwa FROM obiekty WHERE idKontynent = ?');
$zapytanie->bind_param('i', $idKontynent);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
while ($wiersz = $wynik->fetch_assoc()) {
echo '<p>' . $wiersz['nazwa'] . '</p>';
}
$polaczenie->close();
Szkielet – PDO
$pdo = new PDO('mysql:host=localhost;dbname=nazwa_bazy;charset=utf8mb4', 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$zapytanie = $pdo->prepare('SELECT id, nazwa FROM obiekty WHERE idKontynent = :id');
$zapytanie->execute(['id' => $idKontynent]);
foreach ($zapytanie as $wiersz) {
echo '<p>' . $wiersz['nazwa'] . '</p>';
}
$pdo = null;
Rozpoznawanie kodu na pierwszy rzut oka
mysqli_connect(,mysqli_query(,$connjako pierwszy argument → mysqli proceduralnie (stary kod),->bind_param('si', ...),->get_result(),->num_rows→ mysqli obiektowo,new PDO(,:nazwa,->fetchAll(),->rowCount()→ PDO.
Quiz – sprawdź się
1. Z której biblioteki pochodzi każdy fragment?
// A
$w = $st->fetchAll();
// B
$w = $wynik->fetch_all(MYSQLI_ASSOC);
// C
$w = mysqli_fetch_assoc($wynik);
2. Przepisz to zapytanie na PDO (z parametrem nazwanym):
$st = $polaczenie->prepare('SELECT nick FROM turysci WHERE idTurysta = ?');
$st->bind_param('i', $idTurysty);
$st->execute();
3. Dlaczego ten kod się wywali?
$pdo = new PDO('mysql:host=localhost;dbname=wodospady', 'root', '');
$st = $pdo->prepare('SELECT * FROM obiekty WHERE idRodzaj = ?');
$st->bind_param('i', $idRodzaj);
4. Programista twierdzi: „Przepisałem projekt z mysqli na PDO, więc jest teraz odporny na SQL injection”. Czy ma rację? Uzasadnij jednym zdaniem.
5. Co trzeba dopisać na końcu skryptu egzaminacyjnego w każdej z wersji, żeby spełnić wymaganie „na końcu należy zamknąć połączenie z bazą”?
6. Aplikacja ma docelowo działać na MySQL, ale klient zapowiada możliwą migrację na PostgreSQL. Którą bibliotekę wybierasz i czego mimo to nie załatwi za Ciebie ten wybór?
7. Uzupełnij brakujący element, żeby kod działał:
$st = $pdo->prepare('INSERT INTO osiagniecia (idObiekt, idTurysta) VALUES (:obiekt, :turysta)');
$st->execute([________]);
$noweId = ________;
8. Znajdź błąd:
$polaczenie = new mysqli('localhost', 'root', '', 'wodospady');
$st = $polaczenie->prepare('UPDATE turysci SET nick = ? WHERE idTurysta = ?');
$st->bind_param('s', $nick, $id);
$st->execute();
9. Który z tych zapisów kończy pracę z bazą w PDO: close(), = null, disconnect(), unset()? Wskaż wszystkie poprawne.
10. Krótkie zadanie praktyczne: masz gotowy skrypt w mysqli obiektowym wypisujący listę kontynentów w pętli while. Wymień cztery linie/elementy, które trzeba zmienić, żeby przenieść go na PDO.
Odpowiedzi
1. A – PDO, B – mysqli obiektowo, C – mysqli proceduralnie.
2.
$st = $pdo->prepare('SELECT nick FROM turysci WHERE idTurysta = :id');
$st->execute(['id' => $idTurysty]);
3. bind_param() to metoda mysqli — PDO jej nie ma (Call to undefined method PDOStatement::bind_param()). W PDO dane podaje się w execute([...]) albo przez bindValue(). Dodatkowo brakuje tu charset=utf8mb4 w DSN i trybu wyjątków.
4. Nie. Bezpieczeństwo daje stosowanie prepared statements i parametrów — jeśli w PDO dalej skleja zapytanie ze zmiennych ("... WHERE id = $id"), podatność zostaje.
5. mysqli: $polaczenie->close(); PDO: $pdo = null;
6. PDO — zmiana bazy sprowadza się do zmiany DSN i sterownika. Nie załatwi za Ciebie różnic w samym SQL (typy kolumn, funkcje, LIMIT/AUTO_INCREMENT vs odpowiedniki PostgreSQL) — zapytania i tak trzeba przejrzeć.
7.
$st->execute(['obiekt' => $idObiektu, 'turysta' => $idTurysty]);
$noweId = $pdo->lastInsertId();
8. W zapytaniu są dwa ?, a w bind_param() tylko jedna litera typu ('s'). Powinno być bind_param('si', $nick, $id). Brakuje też set_charset('utf8mb4') i zamknięcia połączenia.
9. Poprawne: $pdo = null; oraz unset($pdo);. close() należy do mysqli, disconnect() nie istnieje.
10. Na przykład: (1) new mysqli(...) → new PDO('mysql:host=...;dbname=...;charset=utf8mb4', ...) z opcjami; (2) usunięcie set_charset() (kodowanie jest w DSN); (3) bind_param() + execute() → execute([...]) z parametrem nazwanym; (4) get_result() + while ($w = $wynik->fetch_assoc()) → foreach ($st as $w) albo $st->fetchAll(); (5) close() → = null.