Модули MySQL в PHP: PDO vs mysqli
Основные модули MySQL для PHP
PHP предоставляет два основных расширения для работы с базами данных MySQL: mysqli и PDO. Выбор между ними зависит от требований проекта, однако PDO считается более универсальным и безопасным решением для новых разработок. В статье рассматриваются оба подхода с примерами кода, описанием типичных ошибок и способов их устранения.
Как обеспечить безопасное и гибкое подключение к MySQL в PHP?
Наиболее эффективное решение - использование PDO с подготовленными запросами (prepared statements) и явной обработкой исключений. PDO поддерживает множество СУБД, что делает код легко переносимым, а подготовленные запросы защищают от SQL-инъекций.
Пример подключения к MySQL через PDO:
$host = '127.0.0.1';
$dbname = 'test';
$username = 'root';
$password = '';
$charset = 'utf8mb4';
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=$charset",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
} catch (PDOException $e) {
die("Ошибка подключения: " . $e->getMessage());
}Пояснение: DSN (Data Source Name) содержит тип СУБД, хост, имя базы и кодировку. Режим исключений позволяет отлавливать ошибки через try-catch. После подключения можно выполнять запросы.
Пример запроса SELECT с использованием подготовленных выражений:
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$stmt->execute(['email' => 'user@example.com']);
$user = $stmt->fetch();
print_r($user);Пояснение: Параметры передаются отдельно от SQL, что исключает возможность инъекций. Метод fetch() возвращает одну строку в виде ассоциативного массива (благодаря FETCH_ASSOC).
Типичные ошибки:
- Неправильный DSN - проверьте синтаксис и наличие драйвера pdo_mysql в php.ini.
- Неверные учётные данные - исключение PDOException с кодом 1045 (Access denied).
- Отсутствие драйвера pdo_mysql - ошибка «could not find driver». Установите драйвер через пакетный менеджер или включите расширение в php.ini.
- Проблемы с кодировкой - обязательно указывайте charset в DSN или через SET NAMES.
Как работать с MySQL через объектно-ориентированный интерфейс mysqli?
Расширение mysqli доступно только для MySQL, но предоставляет как объектный, так и процедурный стиль. Для новых проектов с исключительно MySQL бэкендом можно использовать объектный mysqli.
Пример подключения и выполнения запроса:
$mysqli = new mysqli('127.0.0.1', 'root', '', 'test');
if ($mysqli->connect_error) {
die('Ошибка подключения: ' . $mysqli->connect_error);
}
$result = $mysqli->query('SELECT id, name FROM users WHERE email = "user@example.com"');
if ($result) {
$user = $result->fetch_assoc();
print_r($user);
$result->free();
}
$mysqli->close();Пояснение: Прямая вставка значений в SQL-запрос опасна (SQL-инъекции). Рекомендуется использовать подготовленные запросы.
Подготовленный запрос через mysqli:
$stmt = $mysqli->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$email = 'user@example.com';
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
print_r($user);
$stmt->close();Пояснение: Метод bind_param привязывает переменную к плейсхолдеру. Первый аргумент - типы параметров (s - строка, i - целое и т.д.).
Проблемы и ошибки:
- Необходимость явно устанавливать кодировку: $mysqli->set_charset('utf8mb4').
- Порядок аргументов в bind_param - несоответствие количества и типов приводит к ошибке.
- При использовании query() без подготовки уязвимость к инъекциям - всегда предпочитайте подготовленные запросы.
Как использовать mysqli в процедурном стиле для быстрого прототипирования?
Процедурный стиль удобен для быстрых скриптов, где объектно-ориентированный подход избыточен. Все функции предваряются префиксом mysqli_.
Пример:
$link = mysqli_connect('127.0.0.1', 'root', '', 'test');
if (!$link) {
die('Ошибка подключения: ' . mysqli_connect_error());
}
$result = mysqli_query($link, 'SELECT * FROM users');
while ($row = mysqli_fetch_assoc($result)) {
echo $row['name'] . "\n";
}
mysqli_free_result($result);
mysqli_close($link);Пояснение: Процедурный стиль не поддерживает подготовленные запросы напрямую через плейсхолдеры? На самом деле есть mysqli_stmt_init и mysqli_stmt_bind_param, но синтаксис менее удобен.
Проблемы: Путаница между процедурным и объектным стилями в одном проекте. Выбирайте один стиль и придерживайтесь его.
Как обеспечить переносимость кода между разными базами данных?
PDO поддерживает множество СУБД (MySQL, PostgreSQL, SQLite, Oracle и др.). Для смены базы достаточно изменить DSN и, возможно, диалект SQL.
Пример для PostgreSQL:
$pdo = new PDO('pgsql:host=localhost;dbname=test', 'user', 'pass');Пояснение: Код запросов, написанный с использованием PDO и стандартного SQL, останется неизменным. Однако специфические функции MySQL (например, NOW() vs CURRENT_TIMESTAMP) могут потребовать адаптации.
Ошибки: Разные диалекты SQL - некоторые запросы могут не работать на других СУБД. Используйте ANSI-совместимые конструкции или абстрактные слои.
Как реализовать атомарные операции с откатом при ошибке?
Транзакции через PDO:
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();
} catch (PDOException $e) {
$pdo->rollBack();
echo 'Ошибка транзакции: ' . $e->getMessage();
}Пояснение: beginTransaction отключает автоматическую фиксацию. При исключении все изменения откатываются.
Проблемы: Вложенные транзакции не поддерживаются в MySQL. Всегда проверяйте успешность каждого шага внутри транзакции.
Как вызывать хранимые процедуры MySQL из PHP?
Пример для PDO:
$stmt = $pdo->prepare('CALL get_user_by_id(:uid)');
$stmt->execute([':uid' => 1]);
$user = $stmt->fetch();
print_r($user);Пример для mysqli (объектный):
$stmt = $mysqli->prepare('CALL get_user_by_id(?)');
$stmt->bind_param('i', $id);
$id = 1;
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
print_r($user);Пояснение: Хранимые процедуры могут возвращать результирующие наборы. В PDO нужно использовать nextRowset() для перебора множественных наборов.
Ошибки: Если процедура возвращает несколько наборов, забыв вызов nextRowset(), можно получить незакрытый курсор.
Расширенные примеры работы с MySQL через PHP
Ниже приведены подробные примеры, демонстрирующие нетривиальные сценарии.
Массовая вставка записей с транзакцией и подготовленным запросом
$pdo->beginTransaction();
$sql = 'INSERT INTO logs (message, created_at) VALUES (:msg, NOW())';
$stmt = $pdo->prepare($sql);
$messages = ['Лог 1', 'Лог 2', 'Лог 3'];
foreach ($messages as $msg) {
$stmt->execute([':msg' => $msg]);
}
$pdo->commit();
echo 'Вставлено ' . count($messages) . ' записей';Вставлено 3 записей
Пояснение: Транзакция ускоряет выполнение (снижение накладных расходов на фиксацию после каждой вставки) и обеспечивает атомарность.
Выборка с JOIN и преобразованием в ассоциативный массив
$stmt = $pdo->query('
SELECT u.id, u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = "completed"
');
$data = $stmt->fetchAll();
print_r($data);Array
(
[0] => Array
(
[id] => 1
[name] => Alice
[amount] => 150.00
)
[1] => Array
(
[id] => 2
[name] => Bob
[amount] => 200.00
)
)Пояснение: fetchAll() возвращает все строки сразу. Для больших объёмов используйте fetch() в цикле.
Использование PDO::FETCH_CLASS для отображения данных на объект
class User {
public $id;
public $name;
public $email;
}
$stmt = $pdo->prepare('SELECT id, name, email FROM users WHERE id = ?');
$stmt->execute([1]);
$stmt->setFetchMode(PDO::FETCH_CLASS, 'User');
$user = $stmt->fetch();
echo $user->name . ' (' . $user->email . ')';Alice (alice@example.com)
Пояснение: Свойства объекта заполняются до вызова конструктора. Класс должен иметь свойства, соответствующие именам столбцов.
Обработка ошибок с пользовательским исключением
class DatabaseException extends Exception {}
try {
$pdo->exec('INVALID SQL');
} catch (PDOException $e) {
throw new DatabaseException('Ошибка базы данных: ' . $e->getMessage(), (int)$e->getCode());
}Пояснение: Преобразование стандартного исключения в исключение приложения упрощает централизованную обработку.
Работа с BLOB (двоичные данные) – загрузка изображения
$imageData = file_get_contents('photo.jpg');
$stmt = $pdo->prepare('INSERT INTO images (data, mime_type) VALUES (:data, :mime)');
$stmt->bindParam(':data', $imageData, PDO::PARAM_LOB);
$stmt->bindValue(':mime', 'image/jpeg');
$stmt->execute();
echo 'Image inserted, ID: ' . $pdo->lastInsertId();Image inserted, ID: 10
Пояснение: PDO::PARAM_LOB указывает, что параметр - большой объект. Аналогично можно читать BLOB.
Настройка PDO для чтения из реплики и записи на мастер
$masterDsn = 'mysql:host=master.db;dbname=test';
$slaveDsn = 'mysql:host=slave.db;dbname=test';
function getReadConnection() {
global $slaveDsn;
return new PDO($slaveDsn, 'user', 'pass', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
}
function getWriteConnection() {
global $masterDsn;
return new PDO($masterDsn, 'user', 'pass', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
}
// Пример использования
$pdoRead = getReadConnection();
$result = $pdoRead->query('SELECT * FROM users');
$pdoWrite = getWriteConnection();
$pdoWrite->exec('UPDATE users SET visits = visits + 1 WHERE id = 1');Пояснение: Разделение соединений позволяет балансировать нагрузку, но требует осторожности с репликационной задержкой.