SQL-подход

Nette Database предлагает два пути: вы можете писать SQL-запросы сами (SQL-подход) или позволить генерировать их автоматически (см. Explorer). SQL-подход дает вам полный контроль над запросами и при этом обеспечивает их безопасное построение.

Подробности о подключении и конфигурации базы данных можно найти в главе Подключение и конфигурация.

Базовые запросы

Для выполнения запросов к базе данных используется метод query(). Он возвращает объект ResultSet, который представляет результат запроса. В случае сбоя метод выбрасывает исключение. Результат запроса можно перебирать с помощью цикла foreach или использовать одну из вспомогательных функций.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
}

Для безопасной вставки значений в SQL-запросы мы используем параметризованные запросы. Nette Database делает их максимально простыми — достаточно добавить запятую и значение после SQL-запроса:

$database->query('SELECT * FROM users WHERE name = ?', $name);

При наличии нескольких параметров у вас есть два варианта записи. Вы можете либо «перемежать» SQL-запрос параметрами:

$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);

Либо сначала написать весь SQL-запрос, а затем добавить все параметры:

$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);

Защита от SQL Injection

Почему важно использовать параметризованные запросы? Потому что они защищают вас от атаки под названием SQL Injection, при которой злоумышленник мог бы подставить собственные SQL-команды и тем самым получить или повредить данные в базе данных.

Никогда не вставляйте переменные непосредственно в SQL-запрос! Всегда используйте параметризованные запросы, которые защитят вас от SQL Injection.

// ❌ НЕБЕЗОПАСНЫЙ КОД - уязвимый для SQL-инъекций
$database->query("SELECT * FROM users WHERE name = '$name'");

// ✅ Безопасный параметризованный запрос
$database->query('SELECT * FROM users WHERE name = ?', $name);

Ознакомьтесь с возможными рисками безопасности.

Техники запросов

Условия WHERE

Условия WHERE можно записать как ассоциативный массив, где ключи — это имена столбцов, а значения — данные для сравнения. Nette Database автоматически выберет наиболее подходящий SQL-оператор в зависимости от типа значения.

$database->query('SELECT * FROM users WHERE', [
	'name' => 'John',
	'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1

В ключе также можно явно указать оператор для сравнения:

$database->query('SELECT * FROM users WHERE', [
	'age >' => 25,          // использует оператор >
	'name LIKE' => '%John%', // использует оператор LIKE
	'email NOT LIKE' => '%example.com%', // использует оператор NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'

Nette автоматически обрабатывает особые случаи, такие как значения null или массивы.

$database->query('SELECT * FROM products WHERE', [
	'name' => 'Laptop',         // использует оператор =
	'category_id' => [1, 2, 3], // использует IN
	'description' => null,      // использует IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL

Для отрицательных условий используйте оператор NOT:

$database->query('SELECT * FROM products WHERE', [
	'name NOT' => 'Laptop',         // использует оператор <>
	'category_id NOT' => [1, 2, 3], // использует NOT IN
	'description NOT' => null,      // использует IS NOT NULL
	'id' => [],                     // пропускается
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL

Для объединения условий используется оператор AND. Это можно изменить с помощью заполнителя ?or.

Правила ORDER BY

Сортировку ORDER BY можно записать с помощью массива. В ключах указываем столбцы, а значением будет boolean, определяющий, сортировать ли по возрастанию:

$database->query('SELECT id FROM author ORDER BY', [
	'id' => true, // по возрастанию
	'name' => false, // по убыванию
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC

Вставка данных (INSERT)

Для вставки записей используется SQL-команда INSERT.

$values = [
	'name' => 'John Doe',
	'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();

Метод getInsertId() возвращает ID последней вставленной строки. В некоторых базах данных (например, PostgreSQL) необходимо в качестве параметра указать имя последовательности, из которой должен генерироваться ID, с помощью $database->getInsertId($sequenceId).

В качестве параметров можно передавать и speciální hodnoty такие как файлы, объекты DateTime или перечисляемые типы.

Вставка нескольких записей одновременно:

$database->query('INSERT INTO users ?', [
	['name' => 'User 1', 'email' => 'user1@mail.com'],
	['name' => 'User 2', 'email' => 'user2@mail.com'],
]);

Множественная вставка INSERT намного быстрее, так как выполняется один запрос к базе данных вместо множества отдельных.

Предупреждение о безопасности: Никогда не используйте в качестве $values невалидированные данные. Ознакомьтесь с возможными рисками.

Обновление данных (UPDATE)

Для обновления записей используется SQL-команда UPDATE.

// Обновление одной записи
$values = [
	'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);

Количество затронутых строк возвращает $result->getRowCount().

Для UPDATE можно использовать операторы += и -=:

$database->query('UPDATE users SET ? WHERE id = ?', [
	'login_count+=' => 1, // инкремент login_count
], 1);

Пример вставки или обновления записи, если она уже существует. Используем технику ON DUPLICATE KEY UPDATE:

$values = [
	'name' => $name,
	'year' => $year,
];
$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?',
	$values + ['id' => $id],
	$values,
);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
//   ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978

Обратите внимание, что Nette Database распознает, в каком контексте SQL-команды вставляется параметр с массивом, и в соответствии с этим составляет из него SQL-код. Так, из первого массива он составил (id, name, year) VALUES (123, 'Jim', 1978), в то время как второй преобразовал в вид name = 'Jim', year = 1978. Подробнее об этом мы поговорим в разделе Подсказки для построения SQL.

Удаление данных (DELETE)

Для удаления записей используется SQL-команда DELETE. Пример с получением количества удаленных строк:

$count = $database->query('DELETE FROM users WHERE id = ?', 1)
	->getRowCount();

Подсказки для построения SQL

Подсказка — это специальный заполнитель в SQL-запросе, который указывает, как значение параметра должно быть преобразовано в SQL-выражение:

Подсказка Описание Используется автоматически
?name используется для вставки имени таблицы или столбца
?values генерирует (key, ...) VALUES (value, ...) INSERT ... ?, REPLACE ... ?
?set генерирует присваивание key = value, ... SET ?, KEY UPDATE ?
?and объединяет условия в массиве оператором AND WHERE ?, HAVING ?
?or объединяет условия в массиве оператором OR
?order генерирует условие ORDER BY ORDER BY ?, GROUP BY ?

Для динамической вставки имен таблиц и столбцов в запрос используется заполнитель ?name. Nette Database позаботится о правильной обработке идентификаторов в соответствии с конвенциями данной базы данных (например, заключение в обратные кавычки в MySQL).

$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (в MySQL)

Предупреждение: символ ?name используйте только для имен таблиц и столбцов из валидированных входных данных, иначе вы подвергаетесь риску безопасности.

Остальные подсказки обычно указывать не нужно, так как Nette использует умное автоопределение при составлении SQL-запроса (см. третий столбец таблицы). Но вы можете использовать его, например, в ситуации, когда хотите объединить условия с помощью OR вместо AND:

$database->query('SELECT * FROM users WHERE ?or', [
	'name' => 'John',
	'email' => 'john@example.com',
]);
// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com'

Специальные значения

Кроме обычных скалярных типов (string, int, bool), в качестве параметров можно передавать и специальные значения:

  • файлы: fopen('image.gif', 'r') вставляет бинарное содержимое файла
  • дата и время: объекты DateTime преобразуются в формат базы данных
  • перечисляемые типы: экземпляры enum преобразуются в их значение
  • SQL-литералы: созданные с помощью Connection::literal('NOW()') вставляются непосредственно в запрос
$database->query('INSERT INTO articles ?', [
	'title' => 'My Article',
	'published_at' => new DateTime,
	'content' => fopen('image.png', 'r'),
	'state' => Status::Draft,
]);

В базах данных, которые не имеют нативной поддержки типа данных datetime (например, SQLite и Oracle), DateTime преобразуется в значение, указанное в конфигурации базы данных в элементе formatDateTime (значение по умолчанию — U – unix timestamp).

SQL-литералы

В некоторых случаях необходимо указать в качестве значения непосредственно SQL-код, который, однако, не должен восприниматься как строка и экранироваться. Для этого служат объекты класса Nette\Database\SqlLiteral. Их создает метод Connection::literal().

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())

Или альтернативно:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())

SQL-литералы могут содержать параметры:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > ? AND year < ?', $min, $max),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017)

Благодаря чему мы можем создавать интересные комбинации:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('?or', [
		'active' => true,
		'role' => $role,
	]),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin')

Получение данных

Сокращения для запросов SELECT

Для упрощения извлечения данных Connection предлагает несколько сокращений, которые объединяют вызов query() с последующим fetch*(). Эти методы принимают те же параметры, что и query(), то есть SQL-запрос и необязательные параметры. Полное описание методов fetch*() вы найдете ниже.

fetch($sql, ...$params): ?Row Выполняет запрос и возвращает первую строку как объект Row
fetchAll($sql, ...$params): array Выполняет запрос и возвращает все строки как массив объектов Row
fetchPairs($sql, ...$params): array Выполняет запрос и возвращает ассоциативный массив, где первый столбец представляет ключ, а второй — значение
fetchField($sql, ...$params): mixed Выполняет запрос и возвращает значение первого поля из первой строки
fetchList($sql, ...$params): ?array Выполняет запрос и возвращает первую строку как индексированный массив

Пример:

// fetchField() - возвращает значение первой ячейки
$count = $database->query('SELECT COUNT(*) FROM articles')
	->fetchField();

foreach – итерация по строкам

После выполнения запроса возвращается объект ResultSet, который позволяет перебирать результаты несколькими способами. Самый простой способ выполнить запрос и получить строки — это итерация в цикле foreach. Этот способ наиболее экономичен по памяти, так как возвращает данные постепенно и не сохраняет их все сразу в памяти.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
	// ...
}

ResultSet можно итерировать только один раз. Если вам нужно итерировать повторно, сначала необходимо загрузить данные в массив, например, с помощью метода fetchAll().

fetch(): ?Row

Возвращает строку как объект Row. Если больше нет строк, возвращает null. Перемещает внутренний указатель на следующую строку.

$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // считывает первую строку
if ($row) {
	echo $row->name;
}

fetchAll(): array

Возвращает все оставшиеся строки из ResultSet как массив объектов Row.

$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // считывает все строки
foreach ($rows as $row) {
	echo $row->name;
}

fetchPairs (string|int|null $key = null, string|int|null $value = null)array

Возвращает результаты как ассоциативный массив. Первый аргумент указывает имя столбца, который будет использоваться в качестве ключа в массиве, второй аргумент указывает имя столбца, который будет использоваться в качестве значения:

$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]

Если указать только первый параметр, значением будет вся строка, то есть объект Row:

$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]

В случае дублирующихся ключей используется значение из последней строки. При использовании null в качестве ключа массив будет индексирован численно с нуля (тогда коллизий не происходит):

$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]

fetchPairs (Closure $callback)array

Альтернативно, в качестве параметра можно указать callback, который будет для каждой строки возвращать либо само значение, либо пару ключ-значение.

$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]

// Callback также может возвращать массив с парой ключ & значение:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]

fetchField(): mixed

Возвращает значение первого поля из текущей строки. Если больше нет строк, возвращает null. Перемещает внутренний указатель на следующую строку.

$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // считывает имя из первой строки

fetchList(): ?array

Возвращает строку как индексированный массив. Если больше нет строк, возвращает null. Перемещает внутренний указатель на следующую строку.

$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']

getRowCount(): ?int

Возвращает количество затронутых строк последним запросом UPDATE или DELETE. Для SELECT это количество возвращенных строк, но оно может быть неизвестно — в таком случае метод вернет null.

getColumnCount(): ?int

Возвращает количество столбцов в ResultSet.

Информация о запросах

Для отладочных целей можно получить информацию о последнем выполненном запросе:

echo $database->getLastQueryString();   // выводит SQL-запрос

$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString();    // выводит SQL-запрос
echo $result->getTime();           // выводит время выполнения в секундах

Для отображения результата в виде HTML-таблицы можно использовать:

$result = $database->query('SELECT * FROM articles');
$result->dump();

ResultSet предлагает информацию о типах столбцов:

$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();

foreach ($types as $column => $type) {
	echo "$column имеет тип $type->type"; // например, 'id имеет тип int'
}

Логирование запросов

Мы можем реализовать собственное логирование запросов. Событие onQuery — это массив callback-функций, которые вызываются после каждого выполненного запроса:

$database->onQuery[] = function ($database, $result) use ($logger) {
	$logger->info('Запрос: ' . $result->getQueryString());
	$logger->info('Время: ' . $result->getTime());

	if ($result->getRowCount() > 1000) {
		$logger->warning('Большой набор результатов: ' . $result->getRowCount() . ' строк');
	}
};
версия: 4.0