Директен SQL

Можете да работите с Nette Database по два начина: като пишете SQL заявки директно (директен достъп) или като оставите 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 инжектиране.

// ❌ БЕЗОПАСЕН КОД - уязвим към 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 `име` = '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,      // използва 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 обекти или enum типове.

Вмъкване на няколко записа наведнъж:

$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 `име` = 'Jim', `година` = 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 осигурява правилно ескапиране според конвенциите на базата данни (напр. заграждане в задни тирета за 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 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 &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 => Ред(id: 1, име: "John"), 2 => Ред(id: 2, име: "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]);
// ["Джон" => 46, "Джейн" => 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