diff --git a/database/cs/@left-menu.texy b/database/cs/@left-menu.texy index c7b6340b33..74deae2c74 100644 --- a/database/cs/@left-menu.texy +++ b/database/cs/@left-menu.texy @@ -1,5 +1,6 @@ Databáze ******** +- [Úvod |core] - [Direct SQL] - [Explorer] - [Reflexe |reflection] diff --git a/database/cs/core.texy b/database/cs/core.texy index 81897def80..5516c71e27 100644 --- a/database/cs/core.texy +++ b/database/cs/core.texy @@ -1,551 +1,155 @@ -Direct SQL +Nette Database ********** .[perex] -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|explorer]). 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. +Nette Database je výkonná a elegantní databázová vrstva pro PHP, která vyniká svou jednoduchostí použití a chytrými funkcemi. Nevyžaduje žádnou složitou konfiguraci nebo generování entit, s Nette Database můžete začít pracovat okamžitě. +S Nette Database můžete pracovat dvěma způsoby - buď psaním SQL dotazů (Direct přístup), nebo nechat SQL generovat automaticky (Explorer přístup). -Základní použití: - -```php -$database = new Nette\Database\Connection( - 'mysql:host=127.0.0.1;dbname=test', - 'user', - 'password' -); - -$result = $database->query('SELECT * FROM users WHERE name = ?', 'John'); -``` - -.[note] -Informace o vytvoření připojení a konfiguraci najdete na [samostatné stránce |configuration]. - - -Pokládání SQL dotazů -==================== - -Pro dotazování do databáze slouží metoda `query()`. Ta vrací objekt [ResultSet |api:Nette\Database\ResultSet], který reprezentuje výsledek dotazu. V případě selhání metoda [vyhodí výjimku|#Výjimky]. - - -Získávání dat (SELECT) ----------------------- - -Nejjednodušší použití je zavolat `query()` a následně výsledek dotazu, který se vrací jako objekt `ResultSet`, procházet pomocí cyklu `foreach`: - -```php -$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: - -```php -$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: - -```php -$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: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - -Podívejte se, jaké techniky nabízí Nette Database pro [snadný zápis pokročilejších SQL dotazů |#Techniky dotazování]. - - -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. +
+
-.[warning] -**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. +[Direct SQL] +========== +- Bezpečné parametrizované dotazy +- Přesná kontrola nad podobou SQL dotazů +- Když píšete komplexní dotazy s pokročilými funkcemi +- Optimalizujete výkon pomocí specifických SQL funkcí -```php -// ❌ 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 |security]. +
-Vkládání dat (INSERT) ---------------------- +[Explorer] +======== +- Vyvíjíte rychle bez psaní SQL +- Intuitivní práce s relacemi mezi tabulkami +- Oceníte automatickou optimalizaci dotazů +- Vhodné pro rychlou a pohodlnout práci s databází -Pro vkládání záznamů se používá SQL příkaz `INSERT`. - -```php -$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: - -```php -$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 |security#Klíče polí nejsou bezpečné API]. - - -Aktualizace dat (UPDATE) ------------------------- +Instalace +========= -Pro aktualizacizáznamů se používá SQL příkaz `UPDATE`. +Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: -```php -// Aktualizace jednoho záznamu -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); +```shell .[dark] +composer require nette/database ``` -Počet ovlivněných řádků vrátí `$result->getRowCount()`. - -Pro UPDATE můžeme využít operátorů `+=` a `-=`: -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // inkrementace login_count -], 1); -``` +Připojení a konfigurace +======================= -Příklad vložení, nebo úpravy záznamu, pokud již existuje. Použijeme techniku `ON DUPLICATE KEY UPDATE`: +Základní použití: ```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, +$database = new Nette\Database\Connection( + 'mysql:host=127.0.0.1;dbname=test', + 'user', + 'password' ); -// 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|#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ů: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -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()]. - -| `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: - -```php -// 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|api:Nette\Database\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. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} +$result = $database->query('SELECT * FROM users WHERE name = ?', 'John'); ``` .[note] -`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 .[method] ------------------------ - -Vrací řádek jako objekt `Row`. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // načte první řádek -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Vrací všechny zbývající řádky z `ResultSetu` jako pole objektů `Row`. - -```php -$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 .[method] ---------------------------------------------------------------------------------------- - -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: - -```php -$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`: - -```php -$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: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. - -```php -$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 .[method] ------------------------------ - -Vrací hodnotu prvního políčka z aktuálního řádku. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // načte jméno z prvního řádku -``` - - -fetchList(): ?array .[method] ------------------------------ - -Vrací řádek jako indexované pole. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -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 .[method] --------------------------------- - -Vrací počet sloupců v `ResultSetu`. - - -Konverze typů -============= - -Nette Database automaticky konvertuje hodnoty vrácené z databáze na odpovídající PHP typy. - - -Datum a čas ------------ - -Časové údaje jsou převáděny na objekty `Nette\Utils\DateTime`. Pokud chcete, aby byly časové údaje převáděny na immutable objekty `Nette\Database\DateTime`, nastavte v [konfiguraci|configuration] volbu `newDateTime` na true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -V případě MySQL převádí datový typ `TIME` na objekty `DateInterval`. - - -Booleovské hodnoty ------------------- - -Booleovské hodnoty jsou automaticky převedeny na `true` nebo `false`. U MySQL se převádí `TINYINT(1)` pokud nastavíme v [konfiguraci|configuration] `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Číselné hodnoty ---------------- - -Číselné hodnoty jsou převedeny na `int` nebo `float` podle typu sloupce v databázi: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Vlastní normalizace -------------------- - -Pomocí metody `setRowNormalizer(?callable $normalizer)` můžete nastavit vlastní funkci pro transformaci řádků z databáze. To se hodí například pro automatický převod datových typů. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // konverze typů - return $row; -}); -``` - - -Techniky dotazování -=================== - -Nette Database nabízí elegantní a expresivní způsoby, jak sestavovat SQL dotazy. Podívejte se na ně. - - -Podmínky WHERE --------------- +Informace o vytvoření připojení a konfiguraci najdete na [samostatné stránce |configuration]. -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. -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` +Dva přístupy k databázi +=========== -V klíči můžete také explicitně specifikovat operátor pro porovnání: +S Nette Database můžete pracovat dvěma způsoby - buď psaním SQL dotazů (Direct přístup), nebo nechat SQL generovat automaticky (Explorer přístup). -```php -$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. +[Direct přístup|direct-sql] - SQL dotazy ```php -$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 +// SELECT s parametrizovaným dotazem +$result = $database->query('SELECT * FROM users WHERE role = ?', 'admin'); -$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 +// INSERT pomocí pole hodnot +$database->query('INSERT INTO users', [ + 'name' => 'John', + 'email' => 'john@example.com', + 'created_at' => new DateTime, ]); -// 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` (viz níže). - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' OR `active` = 1 +// UPDATE s podmínkou +$database->query('UPDATE users SET', [ + 'active' => true, + 'last_login' => new DateTime, +], 'WHERE id = ?', $userId); ``` +Direct přístup nabízí pomocné nástroje pro bezpečné sestavování dotazů, ale zachovává vám plnou kontrolu nad jejich podobou. -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ě: +[Explorer přístup|explorer] - automatické generování SQL ```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // vzestupně - 'name' => false, // sestupně -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Hinty pro sestavování SQL -------------------------- +// Práce s tabulkou +$users = $database->table('users'); -Hint je speciální zástupný symbol v SQL dotazu, který říká, jak se má hodnota parametru přepsat do SQL výrazu: +// SELECT s podmínkou +$admins = $users->where('role', 'admin'); -| 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 ?` +// Práce se vztahy +foreach ($users as $user) { + echo $user->name; + echo $user->role->description; // automatický JOIN -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). - -```php -$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 |security#Dynamické identifikátory]. - -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`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' + // Výpis článků uživatele + foreach ($user->related('articles') as $article) { + echo $article->title; + } +} ``` +Explorer přístup generuje a optimalizuje SQL dotazy automaticky. Stará se o efektivní načítání dat a práci se vztahy mezi tabulkami. -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: +Oba přístupy lze v aplikaci libovolně kombinovat podle potřeby. -- 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 -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` +Podporované databáze +=========== -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|configuration] položkou `formatDateTime` (výchozí hodnota je `U` - unix timestamp). +Nette Database podporuje následující databáze: +|* Databázový server |* DSN jméno |* Podpora v Explorer +| MySQL (>= 5.1) | mysql | ANO +| PostgreSQL (>= 9.0) | pgsql | ANO +| Sqlite 3 (>= 3.8) | sqlite | ANO +| Oracle | oci | - +| MS SQL (PDO_SQLSRV) | sqlsrv | ANO +| MS SQL (PDO_DBLIB) | mssql | - +| ODBC | odbc | - -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()`. -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` +Správa připojení +================ -Nebo alternativě: +Při vytvoření objektu `Connection` dojde automnaticky k připojení. Pokud chcete připojení odložit, použijte lazy režim - ten zapnete v [konfiguracI|configuration] nastavením `lazy`, nebo takto: ```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) +$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); ``` -SQL literály mohou obsahovat parametry: - -```php -$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) -``` +Pro správu připojení k databázi slouží metody `connect()`, `disconnect()` a `reconnect()`. Metoda `connect()` naváže spojení s databází, pokud ještě není navázáno. Může vyhodit výjimku `Nette\Database\ConnectionException`. Metoda `disconnect()` odpojí se od databáze. Metoda `reconnect()` odpojí se a znovu připojí k databázi. Může vyhodit výjimku `Nette\Database\ConnectionException`. -Díky čemuž můžeme vytvářet zajímavé kombinace: +Kromě toho můžete sledovat události spojené s připojením pomocí události `onConnect`, což je pole callbacků, které se zavolají po navázání spojení s databází. ```php -$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') +// proběhne po připojení k databázi +$database->onConnect[] = function($database) { + echo "Připojeno k databázi"; +}; ``` @@ -653,60 +257,75 @@ try { ``` -Správa připojení -================ +Konverze typů +============= -Při vytvoření objektu `Connection` dojde automnaticky k připojení. Pokud chcete připojení odložit, použijte lazy režim - ten zapnete v [konfiguracI|configuration] nastavením `lazy`, nebo takto: +Nette Database automaticky konvertuje hodnoty vrácené z databáze na odpovídající PHP typy. -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` -Pro správu připojení k databázi slouží metody `connect()`, `disconnect()` a `reconnect()`. Metoda `connect()` naváže spojení s databází, pokud ještě není navázáno. Může vyhodit výjimku `Nette\Database\ConnectionException`. Metoda `disconnect()` odpojí se od databáze. Metoda `reconnect()` odpojí se a znovu připojí k databázi. Může vyhodit výjimku `Nette\Database\ConnectionException`. +Datum a čas +----------- -Kromě toho můžete sledovat události spojené s připojením pomocí události `onConnect`, což je pole callbacků, které se zavolají po navázání spojení s databází. +Časové údaje jsou převáděny na objekty `Nette\Utils\DateTime`. Pokud chcete, aby byly časové údaje převáděny na immutable objekty `Nette\Database\DateTime`, nastavte v [konfiguraci|configuration] volbu `newDateTime` na true. ```php -// proběhne po připojení k databázi -$database->onConnect[] = function($database) { - echo "Připojeno k databázi"; -}; +$row = $database->fetch('SELECT created_at FROM articles'); +echo $row->created_at instanceof DateTime; // true +echo $row->created_at->format('j. n. Y'); ``` +V případě MySQL převádí datový typ `TIME` na objekty `DateInterval`. -Ladění a výkon -============== -Nette Database poskytuje několik užitečných nástrojů pro ladění a optimalizaci výkonu. +Booleovské hodnoty +------------------ +Booleovské hodnoty jsou automaticky převedeny na `true` nebo `false`. U MySQL se převádí `TINYINT(1)` pokud nastavíme v [konfiguraci|configuration] `convertBoolean`. -Tracy Debug Bar +```php +$row = $database->fetch('SELECT is_published FROM articles'); +echo gettype($row->is_published); // 'boolean' +``` + + +Číselné hodnoty --------------- -Pokud používáte [Tracy |tracy:], aktivuje se automaticky panel Database v Debug baru, který zobrazuje všechny provedené dotazy, jejich parametry, dobu vykonání a místo v kódu, kde byly zavolány. +Číselné hodnoty jsou převedeny na `int` nebo `float` podle typu sloupce v databázi: -[* db-panel.webp *] +```php +$row = $database->fetch('SELECT id, price FROM products'); +echo gettype($row->id); // integer +echo gettype($row->price); // float +``` -Informace o dotazu ------------------- +Vlastní normalizace +------------------- -Pro ladicí účely můžeme získat informace o posledním provedeném dotazu: +Pomocí metody `setRowNormalizer(?callable $normalizer)` můžete nastavit vlastní funkci pro transformaci řádků z databáze. To se hodí například pro automatický převod datových typů. ```php -$result = $database->query('SELECT * FROM articles'); - -echo $database->getLastQueryString(); // vypíše SQL dotaz -echo $result->getQueryString(); // vypíše SQL dotaz -echo $result->getTime(); // vypíše dobu vykonání v sekundách +$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { + // konverze typů + return $row; +}); ``` -Pro zobrazení výsledku jako HTML tabulky lze použít: -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` + +Ladění a výkon +============== + +Nette Database poskytuje několik užitečných nástrojů pro ladění a optimalizaci výkonu. + + +Tracy Debug Bar +--------------- + +Pokud používáte [Tracy |tracy:], aktivuje se automaticky panel Database v Debug baru, který zobrazuje všechny provedené dotazy, jejich parametry, dobu vykonání a místo v kódu, kde byly zavolány. + +[* db-panel.webp *] Logování dotazů diff --git a/database/cs/direct-sql.texy b/database/cs/direct-sql.texy new file mode 100644 index 0000000000..5aaeac14a0 --- /dev/null +++ b/database/cs/direct-sql.texy @@ -0,0 +1,501 @@ +Direct SQL +********** + +.[perex] +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|explorer]). 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. + +.[note] +Informace o vytvoření připojení a konfiguraci najdete na [samostatné stránce |configuration]. + + +Pokládání SQL dotazů +==================== + +Pro dotazování do databáze slouží metoda `query()`. Ta vrací objekt [ResultSet |api:Nette\Database\ResultSet], který reprezentuje výsledek dotazu. V případě selhání metoda [vyhodí výjimku|#Výjimky]. + + +Získávání dat (SELECT) +---------------------- + +Nejjednodušší použití je zavolat `query()` a následně výsledek dotazu, který se vrací jako objekt `ResultSet`, procházet pomocí cyklu `foreach`: + +```php +$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: + +```php +$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: + +```php +$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: + +```php +$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); +``` + +Podívejte se, jaké techniky nabízí Nette Database pro [snadný zápis pokročilejších SQL dotazů |#Techniky dotazování]. + + +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. + +.[warning] +**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. + +```php +// ❌ 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 |security]. + + +Vkládání dat (INSERT) +--------------------- + +Pro vkládání záznamů se používá SQL příkaz `INSERT`. + +```php +$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: + +```php +$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 |security#Klíče polí nejsou bezpečné API]. + + +Aktualizace dat (UPDATE) +------------------------ + +Pro aktualizacizáznamů se používá SQL příkaz `UPDATE`. + +```php +// 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 `-=`: + +```php +$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`: + +```php +$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|#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ů: + +```php +$count = $database->query('DELETE FROM users WHERE id = ?', 1) + ->getRowCount(); +``` + + +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()]. + +| `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: + +```php +// 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|api:Nette\Database\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. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; + // ... +} +``` + +.[note] +`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 .[method] +----------------------- + +Vrací řádek jako objekt `Row`. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $database->query('SELECT * FROM users'); +$row = $result->fetch(); // načte první řádek +if ($row) { + echo $row->name; +} +``` + + +fetchAll(): array .[method] +--------------------------- + +Vrací všechny zbývající řádky z `ResultSetu` jako pole objektů `Row`. + +```php +$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 .[method] +--------------------------------------------------------------------------------------- + +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: + +```php +$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`: + +```php +$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: + +```php +$names = $result->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` + + +fetchPairs(Closure $callback): array .[method] +---------------------------------------------- + +Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. + +```php +$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 .[method] +----------------------------- + +Vrací hodnotu prvního políčka z aktuálního řádku. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $database->query('SELECT name FROM users'); +$name = $result->fetchField(); // načte jméno z prvního řádku +``` + + +fetchList(): ?array .[method] +----------------------------- + +Vrací řádek jako indexované pole. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $database->query('SELECT name, email FROM users'); +$row = $result->fetchList(); // ['John', 'john@example.com'] +``` + + +getRowCount(): ?int .[method] +----------------------------- + +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 .[method] +-------------------------------- + +Vrací počet sloupců v `ResultSetu`. + + +Informace o dotazu +------------------ + +Pro ladicí účely můžeme získat informace o posledním provedeném dotazu: + +```php +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: + +```php +$result = $database->query('SELECT * FROM articles'); +$result->dump(); +``` + + +Techniky dotazování +=================== + +Nette Database nabízí elegantní a expresivní způsoby, jak sestavovat SQL dotazy. Podívejte se na 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. + +```php +$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í: + +```php +$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. + +```php +$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 + +$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` (viz níže). + +```php +$database->query('SELECT * FROM users WHERE ?or', [ + 'name' => 'John', + 'active' => true, +]); +// WHERE `name` = 'John' OR `active` = 1 +``` + + +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ě: + +```php +$database->query('SELECT id FROM author ORDER BY', [ + 'id' => true, // vzestupně + 'name' => false, // sestupně +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +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). + +```php +$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 |security#Dynamické identifikátory]. + +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`: + +```php +$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 + +```php +$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|configuration] 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()`. + +```php +$result = $database->query('SELECT * FROM users WHERE', [ + 'name' => $name, + 'year >' => $database::literal('YEAR()'), +]); +// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) +``` + +Nebo alternativě: + +```php +$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: + +```php +$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: + +```php +$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') +``` diff --git a/database/cs/reflection.texy b/database/cs/reflection.texy index eb2a063055..5f95e40136 100644 --- a/database/cs/reflection.texy +++ b/database/cs/reflection.texy @@ -7,7 +7,7 @@ Nette Database poskytuje nástroje pro introspekci databázové struktury pomoc Objekt reflexe získáme z instance připojení k databázi: ```php -$reflection = $connection->getReflection(); +$reflection = $database->getReflection(); ```