Обновляем и удаляем в MySQL из PHP: UPDATE и DELETE с проверкой

Статья посвящена безопасной реализации операций обновления (UPDATE) и удаления (DELETE) данных в MySQL через PHP с использованием PDO. Особое внимание уделено обязательным проверкам: наличию записи, количеству затронутых строк и защите от ошибок.


Важное правило: без WHERE — нельзя

Операции UPDATE и DELETE без условия WHERE применяются ко всей таблице. Одна опечатка может стереть или испортить все данные. Поэтому в коде всегда:

  1. Сначала проверяют, существует ли целевая запись.
  2. Затем выполняют операцию с точным условием.
  3. Проверяют результат (rowCount()).
  4. Обрабатывают ошибки через 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 не существует (в MySQL UPDATE без строк просто вернёт 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():

php
$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('Обновлено больше одной строки — это ошибка логики.');
}

Этот подход короче, но менее нагляден при отладке: вы не видите заранее, существовал ли пользователь. Для критических операций лучше явно проверять существование.


Транзакции: когда нужно несколько операций

Если обновление и удаление — часть более сложной логики (например, «удалить пользователя и все его заказы»), используйте транзакции:

php
$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
<?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.
  • Логируйте технические ошибки, но не показывайте их пользователю.

Добавить комментарий