Базы данных в PHP: от подключения до сложных запросов
Эффективная работа с базами данных в PHP
Организация взаимодействия с базой данных – ключевая задача любого динамического сайта. PHP предоставляет несколько способов подключения и выполнения запросов. Рассмотрим каждый подход с примерами, типичными трудностями и случаями применения.
Основное решение: PDO (PHP Data Objects)
Как гарантировать безопасное и гибкое подключение к любой СУБД?
PDO – интерфейс для доступа к базам данных, поддерживающий множество СУБД (MySQL, PostgreSQL, SQLite и др.). Он использует подготовленные запросы, что исключает SQL-инъекции, и предоставляет единообразный синтаксис.
// Подключение к MySQL через PDO
$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,
];
try {
$pdo = new PDO($dsn, $user, $password, $options);
} catch (PDOException $e) {
die('Ошибка подключения: ' . $e->getMessage());
}
Пояснение: DSN определяет тип СУБД, хост, имя базы и кодировку. Опции настраивают режим ошибок (исключения) и формат выборки (ассоциативный массив). При неудаче выбрасывается исключение.
// Подготовленный запрос с параметрами
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => 'user@example.com']);
$user = $stmt->fetch();
Возможные проблемы: неверные учётные данные, отсутствие драйвера PDO для выбранной СУБД (проверить через phpinfo()).
Ошибка: PDOException: could not find driver
Решение: установить расширение pdo_mysql или соответствующее. В Linux: sudo apt install php-mysql, в Windows раскомментировать extension=pdo_mysql в php.ini.
Случаи использования: новые проекты, миграции между СУБД, требование безопасности.
Вариант 1: mysqli (процедурный стиль)
Как подключиться к MySQL без использования PDO, сохраняя привычный процедурный код?
Расширение mysqli предназначено только для MySQL. Процедурный стиль близок к старому mysql, но поддерживает подготовленные запросы.
$link = mysqli_connect('localhost', 'root', '', 'test');
if (!$link) {
die('Ошибка подключения: ' . mysqli_connect_error());
}
$result = mysqli_query($link, 'SELECT * FROM users');
while ($row = mysqli_fetch_assoc($result)) {
echo $row['name'];
}
mysqli_close($link);
Пояснение: функция mysqli_connect возвращает ресурс соединения. Простые запросы выполняются через mysqli_query, но для подготовки запросов используется mysqli_prepare.
// Подготовленный запрос
$stmt = mysqli_prepare($link, 'SELECT * FROM users WHERE id = ?');
mysqli_stmt_bind_param($stmt, 'i', $id);
$id = 5;
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
Ошибка: Call to undefined function mysqli_connect()
Решение: включить расширение mysqli в php.ini или установить пакет php-mysqli.
Случаи использования: поддержка старого кода, когда PDO нежелательно или проект ориентирован только на MySQL.
Вариант 2: mysqli (объектно-ориентированный стиль)
Как организовать ООП-подход к работе с MySQL без сторонних библиотек?
mysqli предоставляет класс mysqli и связанные объекты.
$mysqli = new mysqli('localhost', 'root', '', 'test');
if ($mysqli->connect_error) {
die('Ошибка: ' . $mysqli->connect_error);
}
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$email = 'test@mail.com';
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo $row['name'];
}
Пояснение: объект $stmt поддерживает цепочку вызовов. Синтаксис более читаемый, чем процедурный.
Проблема: забыть проверить $mysqli->connect_error – скрипт может работать с неверным соединением.
Решение: всегда проверять ошибку соединения и использовать исключения через mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT).
Случаи использования: проекты, требующие объектной модели или строгой типизации.
Вариант 3: Устаревшее расширение mysql (предупреждение)
Почему не стоит применять функции mysql_* в новых разработах?
Расширение mysql удалено из PHP 7.0, оно не поддерживает подготовленные запросы и небезопасно. Пример (для образовательных целей):
$link = mysql_connect('localhost', 'root', '');
mysql_select_db('test', $link);
$result = mysql_query('SELECT * FROM users', $link);
while ($row = mysql_fetch_assoc($result)) {
echo $row['name'];
}
Пояснение: код уязвим к SQL-инъекциям, а функция mysql_connect считается устаревшей.
Ошибка: Fatal error: Call to undefined function mysql_connect()
Решение: перейти на PDO или mysqli. Ретроспективное использование невозможно.
Случаи использования: только для изучения истории, не для продакшена.
Вариант 4: Использование ORM (Doctrine, Eloquent)
Когда удобно применять абстракцию более высокого уровня для взаимодействия с БД?
ORM (Object-Relational Mapping) позволяет работать с таблицами как с объектами. Пример с Eloquent (вне Laravel):
// Composer: illuminate/database
use Illuminate\Database\Capsule\Manager as Capsule;
$capsule = new Capsule;
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'test',
'username' => 'root',
'password' => '',
]);
$capsule->bootEloquent();
class User extends Illuminate\Database\Eloquent\Model {
protected $table = 'users';
}
$users = User::where('active', 1)->get();
Пояснение: ORM скрывает детали SQL, ускоряет разработку, но может порождать медленные запросы при неправильном использовании.
Проблема: N+1 запросов – ORM может выполнять отдельный запрос на каждую связь.
Решение: использовать жадную загрузку (with() в Eloquent).
Случаи использования: большие проекты с множеством сущностей, когда важна скорость разработки, а не максимальная производительность.
Расширенные примеры взаимодействия с базой данных
1. Транзакции и управление ошибками с PDO
$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();
echo 'Перевод выполнен успешно';
} catch (Exception $e) {
$pdo->rollBack();
echo 'Ошибка: ' . $e->getMessage();
}
(при успехе) Перевод выполнен успешно (при ошибке) Ошибка: ...
Пояснение: транзакции гарантируют атомарность изменений. Если один запрос завершается ошибкой, все изменения откатываются.
2. Пакетная вставка нескольких записей с подготовленными запросами
$data = [
['John', 'john@mail.com'],
['Jane', 'jane@mail.com'],
['Bob', 'bob@mail.com'],
];
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
foreach ($data as $row) {
$stmt->execute($row);
}
echo 'Вставлено ' . count($data) . ' записей';
Вставлено 3 записей
Пояснение: один подготовленный запрос, многократно выполняемый с разными данными, эффективнее конкатенации строк.
3. Выборка с JOIN и группировкой
$sql = 'SELECT c.name, COUNT(o.id) AS orders_count
FROM categories c
LEFT JOIN orders o ON o.category_id = c.id
GROUP BY c.id';
$stmt = $pdo->query($sql);
$result = $stmt->fetchAll();
foreach ($result as $row) {
echo $row['name'] . ': ' . $row['orders_count'] . PHP_EOL;
}
Электроника: 15 Одежда: 8 Книги: 3
Пояснение: JOIN объединяет таблицы, GROUP BY агрегирует данные. LEFT JOIN включает категории без заказов.
4. Вызов хранимой процедуры с параметрами
$stmt = $pdo->prepare('CALL sp_get_user_orders(:user_id)');
$stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
$userId = 42;
$stmt->execute();
$orders = $stmt->fetchAll();
print_r($orders);
Array
(
[0] => Array ( [id] => 1 [product] => Ноутбук )
[1] => Array ( [id] => 2 [product] => Мышь )
)
Пояснение: хранимые процедуры позволяют выполнять сложную логику на стороне сервера БД, уменьшая передачу данных.
5. Использование итератора для обработки больших результатов
$stmt = $pdo->prepare('SELECT * FROM logs WHERE date > :date');
$stmt->execute([':date' => '2024-01-01']);
$stmt->setFetchMode(PDO::FETCH_LAZY);
foreach ($stmt as $row) {
// Обработка каждой строки по одной без загрузки всего набора в память
processLog($row['message']);
}
(нет вывода, но обработка происходит построчно)
Пояснение: режим FETCH_LAZY извлекает строки только при обращении, экономя память при большом количестве записей.