Модули MySQL в PHP: PDO vs mysqli

Раздел: PHP программирование -> Базы данных

Основные модули 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');

Пояснение: Разделение соединений позволяет балансировать нагрузку, но требует осторожности с репликационной задержкой.

Модуль MySQL для PHP - comments

En
Php mysql module (php)