Direct SQL
S Nette Database můžete pracovat dvěma způsoby – buď přímo psát SQL dotazy (Direct přístup), nebo nechat SQL generovat automaticky (Explorer přístup). Direct přístup vám pomůže s bezpečným sestavováním dotazů, ale zachovává vám plnou kontrolu nad jejich podobou.
Informace o vytvoření připojení a konfiguraci najdete na samostatné stránce.
Základní dotazování
Pro dotazování do databáze slouží metoda query()
. Ta vrací objekt ResultSet, který reprezentuje výsledek dotazu.
V případě selhání metoda vyhodí výjimku. Výsledek dotazu můžeme procházet pomocí cyklu
foreach
, nebo použít některou z pomocných funkcí.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
Pro bezpečné vkládání hodnot do SQL dotazů používáme parametrizované dotazy. Nette Database je dělá maximálně jednoduché – stačí za SQL dotaz přidat čárku a hodnotu:
$database->query('SELECT * FROM users WHERE name = ?', $name);
Při více parametrech máte dvě možnosti zápisu. Buď můžete SQL dotaz „prokládat“ parametry:
$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);
Nebo napsat nejdříve celý SQL dotaz a pak připojit všechny parametry:
$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);
Ochrana před SQL injection
Proč je důležité používat parametrizované dotazy? Protože vás chrání před útokem zvaným SQL injection, při kterém by útočník mohl podstrčit vlastní SQL příkazy a tím získat nebo poškodit data v databázi.
Nikdy nevkládejte proměnné přímo do SQL dotazu! Vždy používejte parametrizované dotazy, které vás ochrání před SQL injection.
// ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Bezpečný parametrizovaný dotaz
$database->query('SELECT * FROM users WHERE name = ?', $name);
Seznamte se s možnými bezpečnostními riziky.
Techniky dotazování
Podmínky WHERE
Podmínky WHERE můžete zapsat jako asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou data pro porovnání. Nette Database automaticky vybere nejvhodnější SQL operátor podle typu hodnoty.
$database->query('SELECT * FROM users WHERE', [
'name' => 'John',
'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1
V klíči můžete také explicitně specifikovat operátor pro porovnání:
$database->query('SELECT * FROM users WHERE', [
'age >' => 25, // použije operátor >
'name LIKE' => '%John%', // použije operátor LIKE
'email NOT LIKE' => '%example.com%', // použije operátor NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'
Nette automaticky ošetřuje speciální případy jako null
hodnoty nebo pole.
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // použije operátor =
'category_id' => [1, 2, 3], // použije IN
'description' => null, // použije IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL
Pro negativní podmínky použijte operátor NOT
:
$database->query('SELECT * FROM products WHERE', [
'name NOT' => 'Laptop', // použije operátor <>
'category_id NOT' => [1, 2, 3], // použije NOT IN
'description NOT' => null, // použije IS NOT NULL
'id' => [], // vynechá se
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
Pro spojování podmínek se používá operátor AND
. To lze změnit pomocí zástupného symbolu ?or.
Pravidla ORDER BY
Řazení ORDER BY
se dá zapsat pomocí pole. V klíčích uvedeme sloupce a hodnotou bude boolean určující,
zda řadit vzestupně:
$database->query('SELECT id FROM author ORDER BY', [
'id' => true, // vzestupně
'name' => false, // sestupně
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
Vkládání dat (INSERT)
Pro vkládání záznamů se používá SQL příkaz INSERT
.
$values = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();
Metoda getInsertId()
vrátí ID naposledy vloženého řádku. U některých databází (např. PostgreSQL) je
nutné jako parametr specifikovat název sekvence, ze které se má ID generovat pomocí
$database->getInsertId($sequenceId)
.
Jako parametry můžeme předávat i speciální hodnoty jako soubory, objekty DateTime nebo výčtové typy.
Vložení více záznamů najednou:
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
Vícenásobný INSERT je mnohem rychlejší, protože se provede jediný databázový dotaz, namísto mnoha jednotlivých.
Bezpečnostní upozornění: Nikdy nepoužívejte jako $values
nevalidovaná data. Seznamte se s možnými riziky.
Aktualizace dat (UPDATE)
Pro aktualizacizáznamů se používá SQL příkaz UPDATE
.
// Aktualizace jednoho záznamu
$values = [
'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);
Počet ovlivněných řádků vrátí $result->getRowCount()
.
Pro UPDATE můžeme využít operátorů +=
a -=
:
$database->query('UPDATE users SET ? WHERE id = ?', [
'login_count+=' => 1, // inkrementace login_count
], 1);
Příklad vložení, nebo úpravy záznamu, pokud již existuje. Použijeme techniku 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
Všimněte si, že Nette Database pozná, v jakém kontextu SQL příkazu parametr s polem vkládáme a podle toho z něj
sestaví SQL kód. Takže z prvního pole sestavil (id, name, year) VALUES (123, 'Jim', 1978)
, zatímco druhé
převedl do podoby name = 'Jim', year = 1978
. Podroběji se tomu věnujeme v části Hinty pro sestavování SQL.
Mazání dat (DELETE)
Pro mazání záznamů se používá SQL příkaz DELETE
. Příklad se získáním počtu
smazaných řádků:
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
Hinty pro sestavování SQL
Hint je speciální zástupný symbol v SQL dotazu, který říká, jak se má hodnota parametru přepsat do SQL výrazu:
Hint | Popis | Automaticky se použije |
---|---|---|
?name |
použije pro vložení názvu tabulky nebo sloupce | – |
?values |
vygeneruje (key, ...) VALUES (value, ...) |
INSERT ... ? , REPLACE ... ? |
?set |
vygeneruje přiřazení key = value, ... |
SET ? , KEY UPDATE ? |
?and |
spojí podmínky v poli operátorem AND |
WHERE ? , HAVING ? |
?or |
spojí podmínky v poli operátorem OR |
– |
?order |
vygeneruje klauzuli ORDER BY |
ORDER BY ? , GROUP BY ? |
Pro dynamické vkládání názvů tabulek a sloupců do dotazu slouží zástupný symbol ?name
. Nette Database
se postará o správné ošetření identifikátorů podle konvencí dané databáze (např. uzavření do zpětných uvozovek
v MySQL).
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (v MySQL)
Upozornění: symbol ?name
používejte pouze pro názvy tabulek a sloupců z validovaných vstupů, jinak
se vystavujete bezpečnostnímu riziku.
Ostatní hinty obvykle není potřeba uvádět, neboť Nette používá při skládání SQL dotazu chytrou autodetekci (viz
třetí sloupec tabulky). Ale můžete jej využít například v situaci, kdy chcete spojit podmínky pomocí OR
namísto 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'
Speciální hodnoty
Kromě běžných skalárních typů (string, int, bool) můžete jako parametry předávat i speciální hodnoty:
- soubory:
fopen('image.gif', 'r')
vloží binární obsah souboru - datum a čas: objekty
DateTime
se převedou na databázový formát - výčtové typy: instance
enum
se převedou na jejich hodnotu - SQL literály: vytvořené pomocí
Connection::literal('NOW()')
se vloží přímo do dotazu
$database->query('INSERT INTO articles ?', [
'title' => 'My Article',
'published_at' => new DateTime,
'content' => fopen('image.png', 'r'),
'state' => Status::Draft,
]);
U databází, které nemají nativní podporu pro datový typ datetime
(jako SQLite a Oracle), se
DateTime
převádí na hodnotu určenou v konfiguraci databáze položkou
formatDateTime
(výchozí hodnota je U
– unix timestamp).
SQL literály
V některých případech potřebujete jako hodnotu uvést přímo SQL kód, který se ale nemá chápat jako řetězec a
escapovat. K tomuto slouží objekty třídy Nette\Database\SqlLiteral
. Vytváří je metoda
Connection::literal()
.
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())
Nebo alternativě:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())
SQL literály mohou obsahovat parametry:
$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)
Díky čemuž můžeme vytvářet zajímavé kombinace:
$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')
Získání dat
Zkratky pro SELECT dotazy
Pro zjednodušení načítání dat nabízí Connection
několik zkratek, které kombinují volání
query()
s následujícím fetch*()
. Tyto metody přijímají stejné parametry jako
query()
, tedy SQL dotaz a volitelné parametry. Plnohodnotný popis metod fetch*()
najdete níže.
fetch($sql, ...$params): ?Row |
Provede dotaz a vrátí první řádek jako objekt Row |
fetchAll($sql, ...$params): array |
Provede dotaz a vrátí všechny řádky jako pole objektů Row |
fetchPairs($sql, ...$params): array |
Provede dotaz a vrátí asocitivní pole, kde první sloupec představuje klíč a druhý hodnotu |
fetchField($sql, ...$params): mixed |
Provede dotaz a vrátí hodnotu prvního políčka z prvního řádku |
fetchList($sql, ...$params): ?array |
Provede dotaz a vrací první řádek jako indexované pole |
Příklad:
// fetchField() - vrátí hodnotu první buňky
$count = $database->query('SELECT COUNT(*) FROM articles')
->fetchField();
foreach
– iterace přes řádky
Po vykonání dotazu se vrací objekt ResultSet,
který umožňuje procházet výsledky několika způsoby. Nejsnazší způsob, jak vykonat dotaz a získat řádky, je
iterováním v cyklu foreach
. Tento způsob je paměťově nejúspornější, neboť vrací data postupně a
neukládá si je do paměti najednou.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
// ...
}
ResultSet
lze iterovat pouze jednou. Pokud potřebujete iterovat opakovaně, musíte nejprve načíst
data do pole, například pomocí metody fetchAll()
.
fetch(): ?Row
Vrací řádek jako objekt Row
. Pokud už neexistují další řádky, vrací null
. Posune interní
ukazatel na další řádek.
$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // načte první řádek
if ($row) {
echo $row->name;
}
fetchAll(): array
Vrací všechny zbývající řádky z ResultSetu
jako pole objektů Row
.
$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // načte všechny řádky
foreach ($rows as $row) {
echo $row->name;
}
fetchPairs(string|int|null $key = null, string|int|null $value = null): array
Vrátí výsledky jako asociativní pole. První argument určuje název sloupce, který se použije jako klíč v poli, druhý argument určuje název sloupce, který se použije jako hodnota:
$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
Pokud uvedeme pouze první parametr, bude hodnotou celý řádek, tedy objekt Row
:
$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]
Pokud jako klíč uvedeme null
, bude pole indexováno numericky od nuly:
$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs(Closure $callback): array
Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota.
$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]
// Callback může také vracet pole s dvojicí klíč & hodnota:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]
fetchField(): mixed
Vrací hodnotu prvního políčka z aktuálního řádku. Pokud už neexistují další řádky, vrací null
.
Posune interní ukazatel na další řádek.
$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // načte jméno z prvního řádku
fetchList(): ?array
Vrací řádek jako indexované pole. Pokud už neexistují další řádky, vrací null
. Posune interní ukazatel
na další řádek.
$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']
getRowCount(): ?int
Vrací počet ovlivněných řádků posledním dotazem UPDATE
nebo DELETE
. Pro SELECT
je to počet vrácených řádků, ale ten nemusí být znám – v takovém případě metoda vrátí null
.
getColumnCount(): ?int
Vrací počet sloupců v ResultSetu
.
Informace o dotazech
Pro ladicí účely můžeme získat informace o posledním provedeném dotazu:
echo $database->getLastQueryString(); // vypíše SQL dotaz
$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString(); // vypíše SQL dotaz
echo $result->getTime(); // vypíše dobu vykonání v sekundách
Pro zobrazení výsledku jako HTML tabulky lze použít:
$result = $database->query('SELECT * FROM articles');
$result->dump();
ResultSet nabízí informace o typech sloupců:
$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();
foreach ($types as $column => $type) {
echo "$column je typu $type->type"; // např. 'id je typu int'
}
Logování dotazů
Můžeme implementovat vlastní logování dotazů. Událost onQuery
je pole callbacků, které se zavolají po
každém provedeném dotazu:
$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');
}
};