Работа с MySQL в сценариях PHP: от основ до продвинутых приемов
Основные подходы к интеграции MySQL в PHP
Как организовать безопасную и гибкую работу с базой данных?
Наиболее эффективным решением является использование расширения PDO (PHP Data Objects). Оно предоставляет единый интерфейс для работы с разными СУБД, поддерживает подготовленные выражения, транзакции и гибкую обработку ошибок через исключения. PDO считается современным стандартом для взаимодействия PHP с MySQL.
// Подключение к MySQL через PDO
$dsn = 'mysql:host=localhost;dbname=mydb;charset=utf8mb4';
$user = 'root';
$password = '';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$pdo = new PDO($dsn, $user, $password, $options);
После создания объекта PDO выполняются запросы. Подготовленные выражения защищают от SQL-инъекций:
$sql = 'SELECT * FROM users WHERE email = :email';
$stmt = $pdo->prepare($sql);
$stmt->execute([':email' => 'user@example.com']);
$user = $stmt->fetch();
Типичные ошибки: неверный DSN (например, пропущен порт), отсутствие драйвера PDO_mysql в PHP, неправильная обработка исключений (забытый блок try-catch). Решение - проверять наличие расширения через phpinfo(), использовать try-catch и логировать сообщения.
Как использовать MySQLi для подключения и запросов?
MySQLi (MySQL Improved) - нативное расширение для работы только с MySQL. Подходит, когда проект не требует смены СУБД. Существует в объектном и процедурном стилях. Рекомендуется объектный стиль с подготовленными выражениями.
$mysqli = new mysqli('localhost', 'root', '', 'mydb');
if ($mysqli->connect_error) {
die('Ошибка подключения: ' . $mysqli->connect_error);
}
// Подготовленный запрос
$stmt = $mysqli->prepare('SELECT name FROM users WHERE id = ?');
$stmt->bind_param('i', $userId);
$userId = 42;
$stmt->execute();
$stmt->bind_result($name);
$stmt->fetch();
$stmt->close();
Целесообразно применять в легаси-системах или если команда знакома только с MySQLi. Проблемы: путаница между bind_param и bind_result, необходимость явно закрывать Statement, отсутствие единого интерфейса для других баз.
Ошибка: забывают проверять connect_error или не вызывают bind_param с правильными типами (i, s, d, b). Решение - использовать объектный стиль и включить режим исключений через mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT).
Почему не следует применять устаревшие функции mysql_*?
Расширение mysql удалено из PHP 7.0. Оно не поддерживает подготовленные выражения, что делает код уязвимым для SQL-инъекций. Использование этих функций в новых проектах недопустимо. Для совместимости со старыми проектами рекомендуется миграция на PDO или MySQLi.
Проблема: проект работает на древней версии PHP, и разработчик не может обновиться. Решение - в крайнем случае применять экранирование через mysql_real_escape_string, но это не даёт полной защиты. Лучше сразу переписать код на современные расширения.
Какие преимущества даёт использование ORM?
ORM (Object-Relational Mapping) как Doctrine или Eloquent скрывают детали SQL-запросов и позволяют работать с объектами. Подходит для крупных проектов с комплексной бизнес-логикой. Однако ORM добавляет накладные расходы и скрывает оптимизацию запросов.
// Пример с Eloquent (Laravel)
$user = User::where('email', 'user@example.com')->first();
echo $user->name;
Цель: ускорение разработки, автоматическое управление связями, кэширование. Случаи использования: приложения с Active Record-паттерном, микросервисы.
Ошибка: N+1 запросов при ленивой загрузке связанных данных. Решение - использовать eager loading (with) или явные JOIN.
Расширенные примеры работы с MySQL через PDO
Транзакции с точками сохранения (savepoints)
Для сложных операций, где требуется откат до определённой точки, используются savepoints.
$pdo->beginTransaction();
$pdo->exec('INSERT INTO logs (msg) VALUES ("Start")');
$pdo->exec('SAVEPOINT sp1');
$pdo->exec('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
// Если ошибка, откатываем до savepoint
$pdo->exec('ROLLBACK TO SAVEPOINT sp1');
$pdo->commit();
-- Результат: в таблице logs появится запись "Start", а баланс не изменится.
Пояснение: savepoints позволяют частичный откат внутри транзакции, не отменяя предыдущие изменения.
Массовая вставка (bulk insert) с подготовленными выражениями
Для вставки множества записей эффективно использовать один запрос с несколькими наборами параметров.
$data = [
['Alice', 'alice@example.com'],
['Bob', 'bob@example.com'],
['Charlie', 'charlie@example.com']
];
$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
foreach ($data as $row) {
$stmt->execute([':name' => $row[0], ':email' => $row[1]]);
}
$pdo->commit();
Недостаток: каждое выполнение отдельно, но в транзакции это приемлемо. Более быстрый способ - собрать один запрос с несколькими VALUES, но тогда сложнее с параметрами.
$placeholders = [];
$values = [];
foreach ($data as $i => $row) {
$placeholders[] = "(:name$i, :email$i)";
$values[":name$i"] = $row[0];
$values[":email$i"] = $row[1];
}
$sql = 'INSERT INTO users (name, email) VALUES ' . implode(', ', $placeholders);
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
Результат: одна операция вставки, быстрее для больших наборов.
Выборка данных с помощью FETCH_CLASS
Можно автоматически заполнять объекты определённого класса.
class User {
public $id;
public $name;
public $email;
}
$stmt = $pdo->query('SELECT id, name, email FROM users LIMIT 3');
$users = $stmt->fetchAll(PDO::FETCH_CLASS, 'User');
foreach ($users as $user) {
echo $user->name . "\n";
}
Alice Bob Charlie
Пояснение: свойства заполняются до вызова конструктора, если не определить __construct с аргументами.
Обработка ошибок с извлечением SQLSTATE
При использовании PDO::ERRMODE_EXCEPTION исключения содержат код SQLSTATE для анализа.
try {
$pdo->exec('INSERT INTO non_existent_table VALUES (1)');
} catch (PDOException $e) {
$errorCode = $e->getCode(); // SQLSTATE, например '42S02'
$errorMessage = $e->getMessage();
if ($errorCode === '42S02') {
// Таблица не существует
}
}
Полезно для логирования и выборочной обработки различных типов ошибок.
Работа с BLOB-данными (изображения, файлы)
BLOB хранятся как строки, можно использовать подготовленные выражения.
$imageData = file_get_contents('photo.jpg');
$stmt = $pdo->prepare('INSERT INTO images (name, data) VALUES (:name, :data)');
$stmt->bindValue(':name', 'photo.jpg');
$stmt->bindValue(':data', $imageData, PDO::PARAM_LOB);
$stmt->execute();
Для выбора используйте fetchColumn или fetch с указанием PDO::PARAM_LOB.
$stmt = $pdo->prepare('SELECT data FROM images WHERE id = :id');
$stmt->execute([':id' => 1]);
$data = $stmt->fetchColumn();
file_put_contents('output.jpg', $data);
Подключение через Unix-сокет (для локального сервера)
$dsn = 'mysql:unix_socket=/var/run/mysqld/mysqld.sock;dbname=test';
$pdo = new PDO($dsn, 'root', '');
Это может быть быстрее, чем TCP/IP, особенно на локальной машине.
LIKE-запрос с подстановкой через подготовленное выражение
$search = '%john%';
$stmt = $pdo->prepare('SELECT * FROM users WHERE name LIKE :search');
$stmt->execute([':search' => $search]);
$users = $stmt->fetchAll();
Важно: параметр должен содержать символы '%' или '_' уже в значении, так как PDO не экранирует их специально.