Прямой SQL

Вы можете работать с Nette Database двумя способами: писать SQL-запросы напрямую (Direct Access) или использовать автоматическую генерацию SQL(Explorer Access). Прямой доступ позволяет безопасно создавать запросы, сохраняя полный контроль над их структурой.

О создании соединения и его настройке читайте на отдельной странице.

Основные запросы

Метод 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-инъекций

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

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

// ❌ UNSAFE CODE - уязвимость к 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%'

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

$database->query('SELECT * FROM products WHERE', [
	'name' => 'Laptop',         // использует оператор =
	'category_id' => [1, 2, 3], // использует IN
	'description' => null,      // uses 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 может быть задано в виде массива, где ключи представляют столбцы, а значения – булевы, указывающие на порядок возрастания:

$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() возвращает идентификатор последней вставленной строки. Для некоторых баз данных (например, PostgreSQL) необходимо указать имя последовательности с помощью $database->getInsertId($sequenceId).

В качестве параметров можно также передавать специальные значения, такие как файлы, объекты 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('file.png', '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).

Литералы 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 &lt; ?', $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

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

$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - Джон', '2 - Джейн', ...].

// Обратный вызов также может возвращать массив с парой ключ и значение:
$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 is of type $type->type"; // Например, 'id имеет тип int'.
}

Ведение журнала запросов

Вы можете реализовать пользовательское протоколирование запросов. Событие onQuery представляет собой массив обратных вызовов, которые вызываются после каждого выполнения запроса:

$database->onQuery[] = function ($database, $result) use ($logger) {
	$logger->info('Query: ' . $result->getQueryString());
	$logger->info('Time: ' . $result->getTime());

	if ($result->getRowCount() > 1000) {
		$logger->warning('Large result set: ' . $result->getRowCount() . ' rows');
	}
};
версия: 4.0