PDO в PHP: подробное руководство по взаимодействию с базами данных

Раздел: -> Работа с базами данных

Основы PDO и подготовленных запросов

Введение

PDO (PHP Data Objects) - расширение для работы с базами данных, предоставляющее единый интерфейс для разных СУБД. Основное преимущество - использование подготовленных запросов, которые защищают от SQL-инъекций и повышают производительность.

Основное решение: подключение и выполнение запросов через PDO

Создание подключения к MySQL с использованием исключений:

$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$user = 'root';
$password = '';
$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $user, $password, $options);

Пояснение: DSN содержит тип базы, хост, имя БД и кодировку. Опции включают выбрасывание исключений при ошибках, ассоциативный режим выборки и отключение эмуляции подготовленных запросов (реальные prepared statements).

Пример запроса с плейсхолдером:

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => 'user@example.com']);
$user = $stmt->fetch();

Использование именованного плейсхолдера :email. Массив передает значения.

Типичные проблемы:

  • Ошибка "could not find driver" - необходимо установить расширение pdo_mysql.
  • Исключение PDOException при неверных параметрах подключения.
  • Проблемы с кодировкой - всегда указывать charset в DSN.

Как использовать позиционные плейсхолдеры вместо именованных?

Позиционные плейсхолдеры обозначаются знаком вопроса:

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND status = ?');
$stmt->execute(['user@example.com', 'active']);

Порядок значений в массиве соответствует порядку плейсхолдеров. Проблема - легко запутаться при большом количестве параметров. Именованные плейсхолдеры дают большую читаемость.

Ошибка: несоответствие количества плейсхолдеров и переданных значений. PDO выбросит исключение.

Как настроить обработку ошибок без исключений?

Можно установить режим PDO::ERRMODE_WARNING или PDO::ERRMODE_SILENT. Тогда после выполнения запроса проверять код ошибки через $stmt->errorCode(). Пример:

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$stmt = $pdo->prepare('...');
$stmt->execute();
if ($stmt->errorCode() !== '00000') {
    // обработать ошибку
}

Такой подход требует больше кода, но может быть полезен в старых проектах. Однако рекомендуется использовать исключения для упрощения логики.

Проблема: при выключенном выводе ошибок можно пропустить серьезную ошибку, что приведет к неожиданному поведению.

Как получить данные в разных форматах (объект, массив)?

Установка режима выборки через fetch() или fetchAll():

// Ассоциативный массив
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

// Объект stdClass
$stmt->fetch(PDO::FETCH_OBJ);

// В определенный класс
class User {
    public $id;
    public $name;
}
$stmt->fetchAll(PDO::FETCH_CLASS, 'User');

FETCH_CLASS автоматически создает экземпляры класса и заполняет свойства. Важно, чтобы свойства совпадали с именами столбцов.

Ошибка: при использовании FETCH_CLASS с неправильными именами свойств значения не будут присвоены, либо вызовут предупреждение.

Как выполнять транзакции с откатом?

Транзакции гарантируют целостность данных. Пример:

$pdo->beginTransaction();
try {
    $pdo->exec('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
    $pdo->exec('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    // логирование ошибки
}

Все изменения фиксируются только после commit(). В случае ошибки rollBack() отменяет все изменения.

Ошибка: забыть вызвать rollBack() при исключении - БД останется в блокировке или с частичными изменениями. Всегда использовать try-catch.

Как подключиться к SQLite вместо MySQL?

Достаточно изменить DSN:

$dsn = 'sqlite:/path/to/database.db';
$pdo = new PDO($dsn);

Все остальные методы PDO работают одинаково. Однако некоторые функции (например, lastInsertId) могут отличаться.

Проблема: пути к файлу БД должны быть абсолютными, иначе PDO может не найти файл.

Как эффективно вставить много записей?

Использование транзакции и подготовленного запроса внутри цикла:

$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
foreach ($users as $user) {
    $stmt->execute([$user['name'], $user['email']]);
}
$pdo->commit();

Это значительно быстрее, чем выполнение отдельных запросов без транзакции. Время выполнения сокращается в десятки раз.

Ошибка: при очень большом количестве записей (сотни тысяч) может не хватить памяти на хранение запроса. Рекомендуется разбивать на порции.

Пример с использованием savepoint внутри транзакции

Savepoint позволяет частично откатить транзакцию. Пример с имитацией ошибки:

Пример
$pdo->beginTransaction();
$pdo->exec("INSERT INTO logs (action) VALUES ('start')");
$savepoint = 'sp1';
$pdo->exec("SAVEPOINT $savepoint");
$pdo->exec("INSERT INTO users (name) VALUES ('Alice')");
// Допустим, следующий запрос вызывает ошибку
try {
    $pdo->exec("INSERT INTO users (name) VALUES ('Bob')");
    $pdo->exec("INSERT INTO errors (msg) VALUES ('test')");
} catch (PDOException $e) {
    $pdo->exec("ROLLBACK TO SAVEPOINT $savepoint");
    echo "Откат к savepoint, Alice сохранена, Bob нет";
}
$pdo->commit();
(Вывод: "Откат к savepoint, Alice сохранена, Bob нет")

Использование fetch() с курсором для обработки большого набора данных

При выборке миллионов строк fetchAll может исчерпать память. Используйте курсор и fetch в цикле:

Пример
$stmt = $pdo->prepare('SELECT * FROM large_table');
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // обработка строки
    echo $row['id'] . "\n";
}
1
2
3
...

Важно: при использовании emulated prepares (по умолчанию) весь результат все равно может быть загружен в память. Отключите эмуляцию: PDO::ATTR_EMULATE_PREPARES => false.

Пример с использованием PDO::PARAM_INT для явного указания типа

Тип параметра помогает PDO корректно обрабатывать значения, например, для целых чисел:

Пример
$id = '1'; // строка
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();

Если не указать параметр, PDO может передать значение как строку, что в некоторых СУБД может повлиять на индексы.

Обработка NULL с помощью PDO::PARAM_NULL

Пример
$email = null;
$stmt = $pdo->prepare('INSERT INTO users (email) VALUES (:email)');
$stmt->bindValue(':email', $email, PDO::PARAM_NULL);
$stmt->execute();

Явное указание PARAM_NULL гарантирует, что в БД будет записано NULL, а не пустая строка.

Получение последнего вставленного ID с учетом драйвера

Пример
$pdo->exec("INSERT INTO users (name) VALUES ('Test')");
$lastId = $pdo->lastInsertId();
echo $lastId;
42

Для PostgreSQL lastInsertId требует имя последовательности: $pdo->lastInsertId('users_id_seq').

Пример работы с LOB (Large Objects) для загрузки файлов

Пример
$stmt = $pdo->prepare('INSERT INTO files (data) VALUES (:data)');
$fp = fopen('/path/to/file.jpg', 'rb');
$stmt->bindParam(':data', $fp, PDO::PARAM_LOB);
$stmt->execute();
fclose($fp);

PDO::PARAM_LOB передает поток. В MySQL это работает для BLOB полей.

PDO в PHP - comments

En
Pdo php (php)