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 полей.