Статья посвящена безопасной реализации операций обновления (UPDATE) и удаления (DELETE) данных в MySQL через PHP с использованием PDO. Особое внимание уделено обязательным проверкам: наличию записи, количеству затронутых строк и защите от ошибок.
Важное правило: без WHERE — нельзя
Операции UPDATE и DELETE без условия WHERE применяются ко всей таблице. Одна опечатка может стереть или испортить все данные. Поэтому в коде всегда:
- Сначала проверяют, существует ли целевая запись.
- Затем выполняют операцию с точным условием.
- Проверяют результат (
rowCount()). - Обрабатывают ошибки через
try/catch.
UPDATE: обновление с проверкой существования и результата
Сценарий
Нужно обновить email пользователя по его id. Перед обновлением убедимся, что пользователь существует, а после — что действительно обновилась ровно одна строка.
SQL
SELECT id FROM users WHERE id = :id; UPDATE users SET email = :email WHERE id = :id;
PHP-реализация
<?php $dsn = 'mysql:host=localhost;dbname=test_db;charset=utf8mb4'; $user = 'db_user'; $pass = 'db_pass'; try { $pdo = NEW PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); $id = 123; $email = 'new.email@example.com'; // Шаг 1: проверка существования $stmt = $pdo->PREPARE('SELECT id FROM users WHERE id = :id'); $stmt->EXECUTE([':id' => $id]); $row = $stmt->fetch(); IF (!$row) { throw NEW Exception('Пользователь не найден.'); } // Шаг 2: выполнение UPDATE $stmt = $pdo->PREPARE('UPDATE users SET email = :email WHERE id = :id'); $stmt->EXECUTE([ ':email' => $email, ':id' => $id, ]); // Шаг 3: проверка результата $affected = $stmt->rowCount(); IF ($affected !== 1) { throw NEW Exception('Не удалось обновить запись: затронуто строк: ' . $affected); } echo 'Запись успешно обновлена.'; } catch (Exception $e) { // В реальном проекте логируйте $e->getMessage() echo 'Ошибка: операция не выполнена.'; } ?>
Зачем так подробно:
- Проверка
SELECTзащищает от «тихих» ошибок, еслиidне существует (в MySQLUPDATEбез строк просто вернётrowCount() = 0, но это не всегда очевидно). - Проверка
$affected === 1гарантирует, что обновилась именно одна строка, а не ноль или больше.
DELETE: удаление с проверкой и защитой
Сценарий
Удалить пользователя по id, только если он существует, и убедиться, что удалена ровно одна строка.
SQL
SELECT id FROM users WHERE id = :id; DELETE FROM users WHERE id = :id;
PHP-реализация
<?php try { $pdo = NEW PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); $id = 123; // Шаг 1: проверка существования $stmt = $pdo->PREPARE('SELECT id FROM users WHERE id = :id'); $stmt->EXECUTE([':id' => $id]); $row = $stmt->fetch(); IF (!$row) { throw NEW Exception('Пользователь не найден, удалять нечего.'); } // Шаг 2: выполнение DELETE $stmt = $pdo->PREPARE('DELETE FROM users WHERE id = :id'); $stmt->EXECUTE([':id' => $id]); // Шаг 3: проверка результата $deleted = $stmt->rowCount(); IF ($deleted !== 1) { throw NEW Exception('Удаление не прошло корректно: удалено строк: ' . $deleted); } echo 'Пользователь успешно удалён.'; } catch (Exception $e) { echo 'Ошибка: операция не выполнена.'; } ?>
Частые ошибки и как их избежать
| Ошибка | Как проявляется | Решение |
|---|---|---|
UPDATE/DELETE без WHERE |
Меняются/удаляются все строки таблицы | Всегда добавляйте WHERE с уникальным ключом (id) |
Игнорирование rowCount() |
Операция «вроде прошла», но строк не затронуто | Проверяйте $stmt->rowCount() и реагируйте, если не равно 1 |
| Прямая подстановка переменных в SQL | Уязвимость к SQL‑инъекциям | Используйте подготовленные выражения (prepare + execute) |
| Отсутствие проверки существования | Логика ломается, если ID не найден | Делайте предварительный SELECT или используйте логику на уровне приложения |
Практический совет: объединяем проверку и операцию (альтернативный подход)
Иногда вместо отдельного SELECT используют только UPDATE/DELETE и анализируют rowCount():
$stmt = $pdo->PREPARE('UPDATE users SET email = :email WHERE id = :id'); $stmt->EXECUTE([':email' => $email, ':id' => $id]); $affected = $stmt->rowCount(); IF ($affected === 0) { throw NEW Exception('Запись не найдена или не обновлена.'); } ELSEIF ($affected > 1) { throw NEW Exception('Обновлено больше одной строки — это ошибка логики.'); }
Этот подход короче, но менее нагляден при отладке: вы не видите заранее, существовал ли пользователь. Для критических операций лучше явно проверять существование.
Транзакции: когда нужно несколько операций
Если обновление и удаление — часть более сложной логики (например, «удалить пользователя и все его заказы»), используйте транзакции:
$pdo->beginTransaction(); try { // Удаление заказов $stmt = $pdo->PREPARE('DELETE FROM orders WHERE user_id = :id'); $stmt->EXECUTE([':id' => $id]); // Удаление пользователя $stmt = $pdo->PREPARE('DELETE FROM users WHERE id = :id'); $stmt->EXECUTE([':id' => $id]); $pdo->commit(); } catch (Exception $e) { $pdo->ROLLBACK(); // Логирование ошибки echo 'Операция отменена из-за ошибки.'; }
Правила транзакций:
- Все операции внутри транзакции должны быть в одном соединении.
- При любой ошибке делайте
rollBack(). - Не смешивайте DDL (например,
CREATE TABLE) и DML (UPDATE/DELETE) в одной транзакции — в MySQL некоторые DDL автоматически фиксируют транзакцию.
Пример полного безопасного обновления с валидацией
<?php FUNCTION updateUserEmail(PDO $pdo, INT $id, string $email): bool { // Простая валидация формата email IF (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw NEW InvalidArgumentException('Некорректный email.'); } $pdo->beginTransaction(); try { // Проверка существования $stmt = $pdo->PREPARE('SELECT id FROM users WHERE id = :id FOR UPDATE'); $stmt->EXECUTE([':id' => $id]); $row = $stmt->fetch(); IF (!$row) { throw NEW Exception('Пользователь не найден.'); } // Обновление $stmt = $pdo->PREPARE('UPDATE users SET email = :email WHERE id = :id'); $stmt->EXECUTE([':email' => $email, ':id' => $id]); IF ($stmt->rowCount() !== 1) { throw NEW Exception('Обновление не прошло.'); } $pdo->commit(); RETURN TRUE; } catch (Exception $e) { $pdo->ROLLBACK(); throw $e; } } ?>
FOR UPDATE в SELECT блокирует строку на время транзакции и защищает от одновременных изменений.
Вывод
- Всегда используйте
WHEREс уникальными ключами. - Проверяйте существование записи перед операцией либо анализируйте
rowCount(). - Применяйте подготовленные выражения для защиты от SQL‑инъекций.
- Для сложных сценариев используйте транзакции с
commit/rollBack. - Логируйте технические ошибки, но не показывайте их пользователю.
