Обработка результатов запросов MySQL в PHP
Основы получения ответа от MySQL в PHP
Взаимодействие с базой данных MySQL из PHP требует не только отправки запроса, но и корректной обработки полученного ответа. Ответ может содержать набор строк, затронутые строки, последний вставленный идентификатор или информацию об ошибке. Рассмотрим наиболее эффективный подход и альтернативные варианты.
Основное решение: PDO и подготовленные запросы
Современный стандарт для работы с MySQL в PHP - расширение PDO (PHP Data Objects). Оно обеспечивает единый интерфейс для разных баз данных, защиту от SQL-инъекций через подготовленные запросы и гибкую обработку ошибок через исключения.
Вопрос: как получить данные из MySQL с помощью PDO и подготовленных запросов?
<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$user = 'root';
$password = '';
try {
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
$stmt = $pdo->prepare('SELECT id, name, email FROM users WHERE active = :active');
$stmt->execute([':active' => 1]);
$users = $stmt->fetchAll();
foreach ($users as $user) {
echo 'ID: ' . $user['id'] . ', Name: ' . $user['name'] . '<br>';
}
} catch (PDOException $e) {
echo 'Ошибка: ' . $e->getMessage();
}
?>
Возможные проблемы и их решения:
- Ошибка подключения (неверный DSN, хост, логин). Решение: проверить параметры, убедиться, что сервер MySQL запущен.
- Исключение PDOException не обработано. Решение: всегда использовать блок try-catch или установить PDO::ATTR_ERRMODE в ERRMODE_WARNING.
- Неверный параметр в запросе (например, имя плейсхолдера не совпадает). Решение: проверять имена и количество параметров.
- Данные не возвращаются. Решение: проверить, что запрос выполняется (залогировать SQL), и что в таблице есть подходящие записи.
Варианты решений
Вопрос: как получить ответ от MySQL с помощью MySQLi в объектно-ориентированном стиле?
<?php
$mysqli = new mysqli('localhost', 'root', '', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
$result = $mysqli->query('SELECT id, name FROM users WHERE active = 1');
while ($row = $result->fetch_assoc()) {
echo $row['id'] . ' - ' . $row['name'] . '<br>';
}
$result->free();
$mysqli->close();
?>
Особенности: MySQLi имеет как объектный, так и процедурный интерфейс. Для подготовки запросов используйте prepare() и bind_param().
Типичные ошибки: забывают проверить $mysqli->connect_error, используют устаревшие функции (mysql_*), не освобождают результат (free()).
Вопрос: как использовать процедурный MySQLi для получения ответа?
<?php
$link = mysqli_connect('localhost', 'root', '', 'test');
if (!$link) {
die('Ошибка соединения: ' . mysqli_connect_error());
}
$result = mysqli_query($link, 'SELECT * FROM users LIMIT 5');
while ($row = mysqli_fetch_assoc($result)) {
echo $row['name'] . '<br>';
}
mysqli_free_result($result);
mysqli_close($link);
?>
Процедурный стиль удобен для быстрого прототипирования, но в крупных проектах предпочтительнее объектный.
Вопрос: как получить ответ от устаревшего расширения mysql_*?
<?php
$link = mysql_connect('localhost', 'root', '');
if (!$link) {
die('Could not connect: ' . mysql_error());
}
mysql_select_db('test');
$result = mysql_query('SELECT name FROM users');
while ($row = mysql_fetch_array($result)) {
echo $row['name'] . '<br>';
}
mysql_free_result($result);
mysql_close($link);
?>
Важно: расширение mysql_* удалено из PHP 7.0. Использовать его не следует. Мигрируйте на PDO или MySQLi.
Вопрос: как получить одно значение из базы данных (например, COUNT)?
// PDO: fetchColumn
$stmt = $pdo->query('SELECT COUNT(*) FROM users');
$count = $stmt->fetchColumn();
echo 'Всего пользователей: ' . $count;
// MySQLi: fetch_row
$result = $mysqli->query('SELECT MAX(age) FROM users');
$row = $result->fetch_row();
$maxAge = $row[0];
Такой подход эффективен, когда нужен один скалярный результат.
Вопрос: как обработать ошибки выполнения запроса и получить информацию о них?
// PDO с исключениями
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$pdo->exec('INSERT INTO users (name) VALUES ("John")');
} catch (PDOException $e) {
echo 'Ошибка: ' . $e->getMessage();
// Код ошибки: $e->getCode()
}
// MySQLi
if ($mysqli->errno) {
echo 'MySQL error: ' . $mysqli->error;
}
Совет: всегда проверяйте статус выполнения запроса, особенно для INSERT/UPDATE/DELETE. В PDO можно получить количество затронутых строк через rowCount().
Вопрос: как получить ID последней вставленной записи?
// PDO
$pdo->exec('INSERT INTO users (name) VALUES ("Alice")');
$lastId = $pdo->lastInsertId();
// MySQLi
$mysqli->query('INSERT INTO users (name) VALUES ("Bob")');
$lastId = $mysqli->insert_id;
Этот идентификатор полезен для связывания записей.
Расширенные примеры обработки ответа MySQL
1. Пакетная вставка с подготовленными запросами
<?php
$users = [
['name' => 'Anna', 'email' => 'anna@example.com'],
['name' => 'Boris', 'email' => 'boris@example.com'],
['name' => 'Vera', 'email' => 'vera@example.com']
];
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
foreach ($users as $user) {
$stmt->execute($user);
}
$pdo->commit();
echo 'Вставлено записей: ' . count($users);
} catch (Exception $e) {
$pdo->rollBack();
echo 'Ошибка: ' . $e->getMessage();
}
?>
Вставлено записей: 3
Транзакция гарантирует, что все вставки выполнятся атомарно. Если одна из них упадёт, изменения откатятся.
2. Использование fetchAll() с разными режимами
<?php
$stmt = $pdo->query('SELECT id, name FROM users LIMIT 3');
// Ассоциативный массив (ключи - имена столбцов)
$assoc = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Массив объектов
$stmt->execute();
$objects = $stmt->fetchAll(PDO::FETCH_OBJ);
// Массив, где ключ - первый столбец, значение - второй
$stmt->execute();
$pairs = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
print_r($assoc);
print_r($objects);
print_r($pairs);
?>
Array
(
[0] => Array
(
[id] => 1
[name] => Anna
)
[1] => Array
(
[id] => 2
[name] => Boris
)
)
Array
(
[0] => stdClass Object
(
[id] => 1
[name] => Anna
)
[1] => stdClass Object
(
[id] => 2
[name] => Boris
)
)
Array
(
[1] => Anna
[2] => Boris
)
Выбор режима зависит от дальнейшей обработки: FETCH_OBJ удобен для шаблонизаторов, FETCH_KEY_PAIR - для построения карты соответствий.
3. Итерация по большому набору данных без загрузки в память
<?php
// Использование unbuffered query (только при отключённом буферизации)
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->query('SELECT * FROM large_table');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// Обработка каждой строки по одной
echo $row['id'] . '<br>';
}
$stmt->closeCursor();
?>
Осторожно: unbuffered query блокирует таблицу на время итерации и требует полного выбора всех строк до следующего запроса. Используйте только для чтения больших объёмов, когда память ограничена.
4. Обработка NULL значений и типизация
<?php
// Предположим, что в поле phone может быть NULL
$stmt = $pdo->query('SELECT name, phone FROM contacts');
$stmt->setFetchMode(PDO::FETCH_ASSOC);
while ($row = $stmt->fetch()) {
$phone = $row['phone'] ?? 'не указан';
echo $row['name'] . ': ' . $phone . '<br>';
}
// Приведение типов через bindColumn
$stmt = $pdo->prepare('SELECT id, price FROM products WHERE id = ?');
$stmt->execute([$id]);
$stmt->bindColumn('id', $id, PDO::PARAM_INT);
$stmt->bindColumn('price', $price, PDO::PARAM_STR);
$stmt->fetch(PDO::FETCH_BOUND);
echo 'ID: ' . $id . ' (integer), Price: ' . $price . ' (string)';
?>
5. Пример с JOIN и группировкой
<?php
$sql = 'SELECT c.name AS category, COUNT(p.id) AS product_count
FROM categories c
LEFT JOIN products p ON p.category_id = c.id
GROUP BY c.id';
$stmt = $pdo->query($sql);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo '<pre>';
print_r($results);
echo '</pre>';
?>
Array
(
[0] => Array
(
[category] => Electronics
[product_count] => 12
)
[1] => Array
(
[category] => Books
[product_count] => 5
)
)
Этот приём позволяет получить сводную статистику одной выборкой, минимизируя запросы.
6. Обработка ответа с помощью итераторов (интерфейс Traversable)
<?php
// PDOStatement реализует Traversable, поэтому можно использовать в foreach напрямую
$stmt = $pdo->query('SELECT name FROM users');
foreach ($stmt as $row) {
echo $row['name'] . '<br>';
}
// Для инкапсуляции можно создать итератор
class UserIterator implements Iterator {
private $stmt;
public function __construct(PDOStatement $stmt) {
$this->stmt = $stmt;
}
public function current(): array {
return $this->stmt->fetch(PDO::FETCH_ASSOC);
}
public function key(): int { return 0; }
public function next(): void {}
public function rewind(): void { $this->stmt->execute(); }
public function valid(): bool { return $this->current() !== false; }
}
$stmt = $pdo->prepare('SELECT * FROM users WHERE active = ?');
$stmt->execute([1]);
$iterator = new UserIterator($stmt);
foreach ($iterator as $user) {
// обработка
}
?>