diff --git a/database/cs/explorer.texy b/database/cs/explorer.texy index d1f397fd5a..47ef770285 100644 --- a/database/cs/explorer.texy +++ b/database/cs/explorer.texy @@ -3,467 +3,728 @@ Database Explorer
-Nette Database Explorer (dříve Nette Database Table, NDBT) zásadním způsobem zjednodušuje získávání dat z databáze bez nutnosti psát SQL dotazy. +Nette Database Explorer je výkonná vrstva, která zásadním způsobem zjednodušuje získávání dat z databáze bez nutnosti psát SQL dotazy. -- pokládá efektivní dotazy -- nepřenáší zbytečná data -- má elegantní syntax +- Práce s daty je přirozená a snadno pochopitelná +- Generuje optimalizované SQL dotazy, které načítají pouze potřebná data +- Umožňuje snadný přístup k souvisejícím datům bez nutnosti psát JOIN dotazy
-Používání Database Explorer začíná od tabulky a to zavoláním metody `table()` nad objektem [api:Nette\Database\Explorer]. Jak ho nejsnadněji získat je [popsáno tady |core#Připojení a konfigurace], pokud však používáme Nette Database Explorer samostatně, lze jej [vytvořit i ručně|#Ruční vytvoření Explorer]. +Nette Database Explorer je nadstavbou nad nízkoúrovňovou vrstou [Nette Database Core |core], která přidává komfortní objektově-orientovaný přístup k databázi. + +Práce s Explorerem začíná voláním metody `table()` nad objektem [api:Nette\Database\Explorer] (jak ho získat je [popsáno tady |core#Připojení a konfigurace]): ```php -$books = $explorer->table('book'); // jméno tabulky je 'book' +$books = $explorer->table('book'); // 'book' je jméno tabulky ``` -Vrací nám objekt [Selection |api:Nette\Database\Table\Selection], nad kterým můžeme iterovat a projít tak všechny knihy. Řádky jsou instance [ActiveRow |api:Nette\Database\Table\ActiveRow] a data z nich můžeme přímo číst. +Metoda vrací objekt [Selection |api:Nette\Database\Table\Selection], který představuje SQL dotaz. Na tento objekt můžeme navazovat další metody pro filtrování a řazení výsledků. Dotaz se sestaví a spustí až ve chvíli, kdy začneme požadovat data. +Například procházením cyklem `foreach`. Každý řádek je reprezentován objektem [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // výpis sloupce 'title' + echo $book->author_id; // výpis sloupce 'author_id' } ``` -Výběr jednoho konkrétního řádku se provádí pomocí metody `get()`, která vrací přímo instanci ActiveRow. +Explorer zásadním způsobem usnadňuje práci s [vazbami mezi tabulkami |#Vazby mezi tabulkami]. Následující příklad ukazuje, jak snadno můžeme vypsat data z provázaných tabulek (knihy a jejich autoři). Všimněte si, že nemusíme psát žádné JOIN dotazy, Nette je vytvoří za nás: ```php -$book = $explorer->table('book')->get(2); // vrátí knihu s id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Kniha: ' . $book->title; + echo 'Autor: ' . $book->author->name; // vytvoří JOIN na tabulku 'author' +} ``` -Pojďme si vyzkoušet jednoduchý příklad. Potřebujeme z databáze vybrat knihy a jejich autory. To je jednoduchý příklad vazby 1:N. Časté řešení je vybrat data jedním SQL dotazem se spojením tabulek pomocí JOINu. Druhou možností je vybrat data odděleně, jedním dotazem knihy, a poté pro každou knihu vybrat jejího autora (např. pomocí foreach cyklu). To může být optimalizováno do dvou požadavků do databáze, jeden pro knihy a druhý pro autory - a přesně takto to dělá Nette Database Explorer. +Nette Database Explorer optimalizuje dotazy, aby byly co nejefektivnější. Výše uvedený příklad provede pouze dva SELECT dotazy, bez ohledu na to, jestli zpracováváme 10 nebo 10 000 knih. -V níže uvedených příkladech budeme pracovat s databázovým schématem na obrázku. Jsou v něm vazby OneHasMany (1:N) (autor knihy `author_id` a případný překladatel `translator_id`, který může mít hodnotu `null`) a vazba ManyHasMany (M:N) mezi knihou a jejími tagy. +Navíc Explorer sleduje, které sloupce se v kódu používají, a načítá z databáze pouze ty, čímž šetří další výkon. Toto chování je plně automatické a adaptivní. Pokud později upravíte kód a začnete používat další sloupce, Explorer automaticky upraví dotazy. Nemusíte nic nastavovat, ani přemýšlet nad tím, které sloupce budete potřebovat - nechte to na Nette. -[Příklad včetně schématu najdete na GitHubu |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Struktura databáze pro uvedené příklady .<> +Filtrování a řazení +=================== -Následující kód vypíše jméno autora každé knihy a všechny její tagy. Jak přesně to funguje si [povíme za chvíli|#Vazby mezi tabulkami]. +Třída `Selection` poskytuje metody pro filtrování a řazení výběru dat. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Přidá podmínku WHERE. Více podmínek je spojeno operátorem AND +| `whereOr(array $conditions)` | Přidá skupinu podmínek WHERE spojených operátorem OR +| `wherePrimary($value)` | Přidá podmínku WHERE podle primárního klíče +| `order($columns, ...$params)` | Nastaví řazení ORDER BY +| `select($columns, ...$params)` | Specifikuje sloupce, které se mají načíst +| `limit($limit, $offset = null)` | Omezí počet řádků (LIMIT) a volitelně nastaví OFFSET +| `page($page, $itemsPerPage, &$total = null)` | Nastaví stránkování +| `group($columns, ...$params)` | Seskupí řádky (GROUP BY) +| `having($condition, ...$params)` | Přidá podmínku HAVING pro filtrování seskupených řádků -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author je řádek z tabulky 'author' +Metody lze řetězit (tzv. [fluent interface|nette:introduction-to-object-oriented-programming#fluent-interfaces]): `$table->where(...)->order(...)->limit(...)`. - echo 'tags: '; - foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name . ', '; // $bookTag->tag je řádek z tabulky 'tag' - } -} -``` +V těchto metodách můžete také používat speciální notaci pro přístup k [datům ze souvisejících tabulek|#Dotazování přes související tabulky]. -Příjemně vás překvapí, jak efektivně databázová vrstva pracuje. Výše uvedený příklad provede konstantní počet požadavků, které vypadají takto: -```sql -SELECT * FROM `book` -SELECT * FROM `author` WHERE (`author`.`id` IN (11, 12)) -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3)) -SELECT * FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23)) -``` +Escapování a identifikátory +--------------------------- -Pokud použijete [cache |caching:] (ve výchozím nastavení je zapnutá), nebudou z databáze načítány žádné nepotřebné sloupce. Po prvním dotazu se do cache uloží jména použitých sloupců a dále budou z databáze vybírány pouze ty sloupce, které skutečně použijete: +Metody automaticky escapují parametry a uvozují identifikátory (názvy tabulek a sloupců), čímž zabraňuje SQL injection. Pro správné fungování je nutné dodržovat několik pravidel: -```sql -SELECT `id`, `title`, `author_id` FROM `book` -SELECT `id`, `name` FROM `author` WHERE (`author`.`id` IN (11, 12)) -SELECT `book_id`, `tag_id` FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3)) -SELECT `id`, `name` FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23)) +- Klíčová slova, názvy funkcí, procedur apod. pište **velkými písmeny**. +- Názvy sloupců a tabulek pište **malými písmeny**. +- Řetězce vždy dosazujte přes **parametry**. + +```php +where('name = ' . $name); // KATASTROFA: zranitelné vůči SQL injection +where('name LIKE "%search%"'); // ŠPATNĚ: komplikuje automatické uvozování +where('name LIKE ?', '%search%'); // SPRÁVNĚ: hodnota dosazená přes parametr + +where('name like ?', $name); // ŠPATNĚ: vygeneruje: `name` `like` ? +where('name LIKE ?', $name); // SPRÁVNĚ: vygeneruje: `name` LIKE ? +where('LOWER(name) = ?', $value);// SPRÁVNĚ: LOWER(`name`) = ? ``` -Výběry -====== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Podívejme se na možnosti filtrování a omezování výběru pomocí třídy [api:Nette\Database\Table\Selection]: +Filtruje výsledky pomocí podmínek WHERE. Její silnou stránkou je inteligentní práce s různými typy hodnot a automatická volba SQL operátorů. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Nastaví WHERE s použitím AND jako spojovatele při více než jedné podmínce -| `$table->whereOr($where)` | Nastaví WHERE s použitím OR jako spojovatele při více než jedné podmínce -| `$table->order($columns)` | Nastaví ORDER BY, může být výraz `('column DESC, id DESC')` -| `$table->select($columns)` | Nastaví vrácené sloupce, může být výraz `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Nastaví LIMIT a OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Nastaví stránkování -| `$table->group($columns)` | Nastaví GROUP BY -| `$table->having($having)` | Nastaví HAVING +Základní použití: -Můžeme použít tzv. [fluent interface|nette:introduction-to-object-oriented-programming#fluent-interfaces], například `$table->where(...)->order(...)->limit(...)`. Vícenásobné `where` nebo `whereOr` podmínky je spojeny operátorem `AND`. +```php +$table->where('id', $value); // WHERE `id` = 123 +$table->where('id > ?', $value); // WHERE `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' +``` +Díky automatické detekci vhodných operátorů nemusíme řešit různé speciální případy. Nette je vyřeší za nás: -where() -------- +```php +$table->where('id', 1); // WHERE `id` = 1 +$table->where('id', null); // WHERE `id` IS NULL +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +// lze použít i zástupný otazník bez operátoru: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer automaticky přidá vhodné operátory podle toho, jaká data dostane: +Metoda správně zpracovává i záporné podmínky a prázdné pole: -.[language-php] -| `$table->where('field', $value)` | field = $value -| `$table->where('field', null)` | field IS NULL -| `$table->where('field > ?', $val)` | field > $val -| `$table->where('field', [1, 2])` | field IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OR name = 'Jon Snow' -| `$table->where('field', $explorer->table($tableName))` | field IN (SELECT $primary FROM $tableName) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | field IN (SELECT col FROM $tableName) +```php +$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nic nenalezne +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- nalezene vše +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- nalezene vše +// $table->where('NOT id ?', $ids); Pozor - tato syntaxe není podporovaná +``` -Zástupný symbol (otazník) funguje i bez sloupcového operátoru. Následující volání jsou stejná: +Jako parametr můžeme předat také výsledek z jiné tabulky - vytvoří se poddotaz: ```php -$table->where('id = ? OR id = ?', 1, 2); -$table->where('id ? OR id ?', 1, 2); +// WHERE `id` IN (SELECT `id` FROM `tableName`) +$table->where('id', $explorer->table($tableName)); + +// WHERE `id` IN (SELECT `col` FROM `tableName`) +$table->where('id', $explorer->table($tableName)->select('col')); ``` -Díky tomu lze generovat správný operátor na základě hodnoty: +Podmínky můžeme předat také jako pole, jehož položky se spojí pomocí AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Selection správně zpracovává i záporné podmínky a umí pracovat také s prázdnými poli: +V poli můžeme použít dvojice klíč => hodnota a Nette opět automaticky zvolí správné operátory: ```php -$table->where('id', []); // id IS NULL AND FALSE -$table->where('id NOT', []); // id IS NULL OR TRUE -$table->where('NOT (id ?)', $ids); // NOT (id IS NULL AND FALSE) +// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` -// toto způsobí výjimku, tato syntax není podporovaná -$table->where('NOT id ?', $ids); +V poli můžeme kombinovat SQL výrazy se zástupnými otazníky a více parametry. To je vhodné pro komplexní podmínky s přesně definovanými operátory: + +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // dva parametry předáme jako pole +]); ``` +Vícenásobné volání `where()` podmínky automaticky spojuje pomocí AND. -whereOr() ---------- -Příklad použití bez parametrů: +whereOr(array $parameters): static .[method] +-------------------------------------------- +Podobně jako `where()` přidává podmínky, ale s tím rozdílem, že je spojuje pomocí OR: ```php -// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`)) +// WHERE (`status` = 'active') OR (`deleted` = 1) $table->whereOr([ - 'user_id IS NULL', - 'SUM(field1) > SUM(field2)', + 'status' => 'active', + 'deleted' => true, ]); ``` -Použijeme parametry. Pokud neuvedeme operátor, Nette Database Explorer automaticky přidá vhodný: +I zde můžeme použít komplexnější výrazy: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -V klíči lze uvést výraz obsahující zástupné otazníky a v hodnotě pak předáme parametry: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ +Přidá podmínku pro primární klíč tabulky: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// WHERE `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Pokud má tabulka kompozitní primární klíč (např. `foo_id`, `bar_id`), předáme jej jako pole: + +```php +// WHERE `foo_id` = 1 AND `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); + +// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) +$table->wherePrimary([ + ['foo_id' => 1, 'bar_id' => 5], + ['foo_id' => 2, 'bar_id' => 3], +])->fetchAll(); ``` -order() -------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Příklady použití: +Určuje pořadí, v jakém budou řádky vráceny. Můžeme řadit podle jednoho či více sloupců, v sestupném či vzestupném pořadí, nebo podle vlastního výrazu: ```php -$table->order('field1'); // ORDER BY `field1` -$table->order('field1 DESC, field2'); // ORDER BY `field1` DESC, `field2` -$table->order('field = ? DESC', 123); // ORDER BY `field` = 123 DESC +$table->order('created'); // ORDER BY `created` +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` +$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC ``` -select() --------- +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Specifikuje sloupce, které se mají vrátit z databáze. Ve výchozím stavu Nette Database Explorer vrací pouze ty sloupce, které se reálně použijí v kódu. Metodu `select()` tak používáme v případech, kdy potřebujeme vrátit specifické výrazy: + +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` -Příklady použití: +Aliasy definované pomocí `AS` jsou pak dostupné jako vlastnosti objektu ActiveRow: ```php -$table->select('field1'); // SELECT `field1` -$table->select('col, UPPER(col) AS abc'); // SELECT `col`, UPPER(`col`) AS abc -$table->select('SUBSTR(title, ?)', 3); // SELECT SUBSTR(`title`, 3) +foreach ($table as $row) { + echo $row->formatted_date; // přístup k aliasu +} ``` -limit() -------- +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Příklady použití: +Omezuje počet vrácených řádků (LIMIT) a volitelně umožňuje nastavit offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (vrátí prvních 10 řádků) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Pro stránkování je vhodnější použít metodu `page()`. + -page() ------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Alternativní způsob pro nastavení limitu a offsetu: +Usnadňuje stránkování výsledků. Přijímá číslo stránky (počítané od 1) a počet položek na stránku. Volitelně lze předat referenci na proměnnou, do které se uloží celkový počet stránek: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Celkem stránek: $numOfPages"; ``` -Získání čísla poslední stránky, předá se do proměnné `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Seskupuje řádky podle zadaných sloupců (GROUP BY). Používá se obvykle ve spojení s agregačními funkcemi: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Spočítá počet produktů v každé kategorii +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() -------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Příklady použití: +Nastavuje podmínku pro filtrování seskupených řádků (HAVING). Lze ji použít ve spojení s metodou `group()` a agregačními funkcemi: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Nalezne kategorie, které mají více než 100 produktů +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() --------- +Čtení dat +========= + +Pro čtení dat z databáze máme k dispozici několik užitečných metod: + +.[language-php] +| `foreach ($table as $key => $row)` | Iteruje přes všechny řádky, `$key` je hodnota primárního klíče, `$row` je objekt ActiveRow +| `$row = $table->get($key)` | Vrátí jeden řádek podle primárního klíče +| `$row = $table->fetch()` | Vrátí aktuální řádek a posune ukazatel na další +| `$array = $table->fetchPairs()` | Vytvoří asociativní pole z výsledků +| `$array = $table->fetchAll()` | Vráti všechny řádky jako pole +| `count($table)` | Vrátí počet řádků v objektu Selection + +Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je určen pouze pro čtení. To znamená, že nelze měnit hodnoty jeho properties. Toto omezení zajišťuje konzistenci dat a zabraňuje neočekávaným vedlejším efektům. Data se načítají z databáze a jakákoliv změna by měla být provedena explicitně a kontrolovaně. + + +`foreach` - iterace přes všechny řádky +-------------------------------------- -Příklady použití: +Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterováním v cyklu `foreach`. Automaticky spouští SQL dotaz. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key je hodnota primárního klíče, $book je ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Výběry hodnotou z jiné tabulky .[#toc-joining-key] --------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Vykoná SQL dotaz a vrátí řádek podle primárního klíče, nebo `null`, pokud neexistuje. + +```php +$book = $explorer->table('book')->get(123); // vrátí ActiveRow s ID 123 nebo null +if ($book) { + echo $book->title; +} +``` + -Často potřebujeme filtrovat výsledky pomocí podmínky, která zahrnuje jinou databázovou tabulku. Tento typ podmínek vyžaduje spojení tabulek, s Nette Database Explorer už je ale nikdy nemusíme psát ručně. +fetch(): ?ActiveRow .[method] +----------------------------- -Řekněme, že chceme vybrat všechny knihy, které napsal autor jménem `Jon`. Musíme napsat pouze jméno spojovacího klíče relace a název sloupce spojené tabulky. Spojovací klíč je odvozen od jména sloupce, který odkazuje na tabulku, se kterou se chceme spojit. V našem příkladu (viz databázové schéma) je to sloupec `author_id`, ze kterého stačí použít část - `author`. `name` je název sloupce v tabulce `author`. Můžeme vytvořit podmínku také pro překladatele knihy, který je připojen sloupcem `translator_id`. +Vrací jeden řádek a posune interní ukazatel na další. Pokud už neexistují další řádky, vrací `null`. ```php $books = $explorer->table('book'); -$books->where('author.name LIKE ?', '%Jon%'); -$books->where('translator.name', 'David Grudl'); +while ($book = $books->fetch()) { + $this->processBook($book); +} ``` -Logika vytváření spojovacího klíče je dána implementací [Conventions |api:Nette\Database\Conventions]. Doporučujeme použití [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], které analyzuje cizí klíče a umožňuje jednoduše pracovat se vztahy mezi tabulkami. -Vztah mezi knihou a autorem je 1:N. Obrácený vztah je také možný, nazýváme ho **backjoin**. Podívejme se na následující příklad. Chceme vybrat všechny autory, kteří napsali více než tři knihy. Pro vytvoření obráceného spojení použijeme `:` (dvojtečku). Dvojtečka znamená, že jde o vztah hasMany (a je to logické, dvě tečky jsou více než jedna). Bohužel třída Selection není dostatečně chytrá a musíme mu pomoci s agregací výsledků a předat mu část `GROUP BY`, také podmínka musí být zapsaná jako `HAVING`. +fetchPairs(): 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 -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book.id) > 3'); +$authors = $explorer->table('author')->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Možná jste si všimli, že spojovací výraz odkazuje na `book`, ale není jasné, jestli spojujeme přes `author_id` nebo `translator_id`. Ve výše uvedeném příkladu Selection spojuje přes sloupec `author_id`, protože byla nalezena shoda se jménem zdrojové tabulky - tabulky `author`. Pokud by neexistovala shoda a existovalo více možností, Nette vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Pokud je zadán pouze název sloupce pro klíč, bude hodnotou celý řadek, tedy objekt `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` -Abychom mohli spojovat přes `translator_id`, stačí přidat volitelný parametr do spojovacího výrazu. +Pokud jako klíč uvedeme `null`, bude pole indexováno numericky od nuly: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` + +Jako parametr můžeme také uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. Pokud callback vrací pouze hodnotu, klíčem bude primární klíč řádku: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'První kniha (Jan Novák)', ...] + +// Callback může také vracet pole s dvojicí klíč & hodnota: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['První kniha' => 'Jan Novák', ...] ``` -Teď se podívejme na složitější příklad na skládání tabulek. -Chceme vybrat všechny autory, kteří napsali něco o PHP. Všechny knihy mají štítky, takže chceme vybrat všechny autory, kteří napsali knihu se štítkem 'PHP'. +fetchAll(): array .[method] +--------------------------- + +Vrátí všechny řádky jako asociativní pole objektů `ActiveRow`, kde klíče jsou hodnoty primárních klíčů. ```php -$authors = $explorer->table('author'); -$authors->where(':book:book_tags.tag.name', 'PHP') - ->group('author.id') - ->having('COUNT(:book:book_tags.tag.id) > 0'); +$allBooks = $explorer->table('book')->fetchAll(); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Agregace výsledků ------------------ +count(): int .[method] +---------------------- -| `$table->count('*')` | Vrátí počet řádků -| `$table->count("DISTINCT $column")` | Vrátí počet odlišných hodnot -| `$table->min($column)` | Vrátí minimální hodnotu -| `$table->max($column)` | Vrátí maximální hodnotu -| `$table->sum($column)` | Vrátí součet všech hodnot -| `$table->aggregation("GROUP_CONCAT($column)")` | Pro jakoukoliv jinou agregační funkci +Metoda `count()` bez parametru vrací počet řádků v objektu `Selection`: -.[caution] -Metoda `count()` bez uvedeného parametru vybere všechny záznamy a vrátí velikost pole, což je velmi neefektivní. Pokud potřebujete například spočítat počet řádků pro stránkování, vždy první argument uveďte. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativa +``` +Pozor, `count()` s parametrem provádí agregační funkci COUNT v databázi, viz níže. -Escapování a uvozovky -===================== -Database Explorer umí chytře escapovat parametry a identifikátory. Pro správnou funkčnost je ale nutno dodržovat několik pravidel: +ActiveRow::toArray(): array .[method] +------------------------------------- -- klíčová slova, názvy funkcí, procedur apod. psát velkými písmeny -- názvy sloupečků a tabulek psát malými písmeny -- hodnoty dosazovat přes parametry +Převede objekt `ActiveRow` na asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou odpovídající data. ```php -->where('name like ?', 'John'); // ŠPATNĚ! vygeneruje: `name` `like` ? -->where('name LIKE ?', 'John'); // SPRÁVNĚ +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray bude ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Agregace +======== + +Třída `Selection` poskytuje metody pro snadné provádění agregačních funkcí (COUNT, SUM, MIN, MAX, AVG atd.). + +.[language-php] +| `count($expr)` | Spočítá počet řádků +| `min($expr)` | Vrátí minimální hodnotu ve sloupci +| `max($expr)` | Vrátí maximální hodnotu ve sloupci +| `sum($expr)` | Vrátí součet hodnot ve sloupci +| `aggregation($function)` | Umožňuje provést libovolnou agregační funkci. Např. `AVG()`, `GROUP_CONCAT()` -->where('KEY = ?', $value); // ŠPATNĚ! KEY je klíčové slovo -->where('key = ?', $value); // SPRÁVNĚ. vygeneruje: `key` = ? -->where('name = ' . $name); // ŠPATNĚ! sql injection! -->where('name = ?', $name); // SPRÁVNĚ +count(string $expr): int .[method] +---------------------------------- -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // ŠPATNĚ! hodnoty dosazujeme přes parametr -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // SPRÁVNĚ +Provede SQL dotaz s funkcí COUNT a vrátí výsledek. Metoda se používá k zjištění, kolik řádků odpovídá určité podmínce: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -.[warning] -Špatné použití může vést k bezpečnostním dírám v aplikaci. +Pozor, [#count()] bez parametru pouze vrací počet řádků v objektu `Selection`. -Čtení dat -========= +min(string $expr) a max(string $expr) .[method] +----------------------------------------------- -| `foreach ($table as $id => $row)` | Iteruje přes všechny řádky výsledku -| `$row = $table->get($id)` | Vrátí jeden řádek s ID $id -| `$row = $table->fetch()` | Vrátí další řádek výsledku -| `$array = $table->fetchPairs($key, $value)` | Vrátí všechny výsledky jako asociativní pole -| `$array = $table->fetchPairs($value)` | Vrátí všechny řádky jako asociativní pole -| `$array = $table->fetchPairs($callable)` | Callback vrací `[$value]` nebo `[$key, $value]` -| `count($table)` | Vrátí počet řádků výsledku +Metody `min()` a `max()` vrací minimální a maximální hodnotu ve specifikovaném sloupci nebo výrazu: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr) .[method] +--------------------------- + +Vrací součet hodnot ve specifikovaném sloupci nebo výrazu: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` + + +aggregation(string $function, ?string $groupFunction = null) .[method] +---------------------------------------------------------------------- + +Umožňuje provést libovolnou agregační funkci. + +```php +// průměrná cena produktů v kategorii +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// spojí štítky produktu do jednoho řetězce +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Pokud potřebujeme agregovat výsledky, které už samy o sobě vzešly z nějaké agregační funkce a seskupení (např. `SUM(hodnota)` přes seskupené řádky), jako druhý argument uvedeme agregační funkci, která se má na tyto mezivýsledky aplikovat: + +```php +// Vypočítá celkovou cenu produktů na skladě pro jednotlivé kategorie a poté sečte tyto ceny dohromady. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +V tomto příkladu nejprve vypočítáme celkovou cenu produktů v každé kategorii (`SUM(price * stock) AS category_total`) a seskupíme výsledky podle `category_id`. Poté použijeme `aggregation('SUM(category_total)', 'SUM')` k sečtení těchto mezisoučtů `category_total`. Druhý argument `'SUM'` říká, že se má na mezivýsledky aplikovat funkce SUM. Insert, Update & Delete ======================= -Metoda `insert()` přijímá pole nebo Traversable objekty (například [ArrayHash |utils:arrays#ArrayHash] se kterým pracují [formuláře |forms:]): +Nette Database Explorer zjednodušuje vkládání, aktualizaci a mazání dat. Všechny uvedené metody v případě vyhodí výjimku `Nette\Database\DriverException`. + + +Selection::insert(iterable $data) .[method] +------------------------------------------- + +Vloží nové záznamy do tabulky. + +**Vkládání jednoho záznamu:** + +Nový záznam předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce. + +Pokud má tabulka definovaný primární klíč, metoda vrací objekt `ActiveRow`, který se znovunačte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (triggery, výchozí hodnoty sloupců, výpočty auto-increment sloupců). Tím je zajištěna konzistence dat a objekt vždy obsahuje aktuální data z databáze. Pokud jednoznačný primární klíč nemá, vrací předaná data ve formě pole. ```php $row = $explorer->table('users')->insert([ - 'name' => $name, - 'year' => $year, + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', ]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) +// $row je instance ActiveRow a obsahuje kompletní data vloženého řádku, +// včetně automaticky generovaného ID a případných změn provedených triggery +echo $row->id; // Vypíše ID nově vloženého uživatele +echo $row->created_at; // Vypíše čas vytvoření, pokud je nastaven triggerem ``` -Má-li tabulka definovaný primární klíč, vrací nový řádek jako objekt ActiveRow. +**Vkládání více záznamů najednou:** -Vícenásobný insert: +Metoda `insert()` umožňuje vložit více záznamů pomocí jednoho SQL dotazu. V tomto případě vrací počet vložených řádků. ```php -$explorer->table('users')->insert([ +$insertedRows = $explorer->table('users')->insert([ + [ + 'name' => 'John', + 'year' => 1994, + ], [ - 'name' => 'Jim', - 'year' => 1978, - ], [ 'name' => 'Jack', - 'year' => 1987, + 'year' => 1995, ], ]); -// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) +// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows bude 2 +``` + +Jako parametr lze také předat objekt `Selection` s výběrem dat. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Jako parametry můžeme předávat i soubory nebo objekty DateTime: +**Vkládání speciálních hodnot:** + +Jako hodnoty můžeme předávat i soubory, objekty DateTime nebo SQL literály: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // nebo $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // vloží soubor + 'name' => 'John', + 'created_at' => new DateTime, // převede na databázový formát + 'avatar' => fopen('image.jpg', 'rb'), // vloží binární obsah souboru + 'uuid' => $explorer::literal('UUID()'), // zavolá funkci UUID() ]); ``` -Úprava záznamů (vrací počet změněných řádků): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Aktualizuje řádky v tabulce podle zadaného filtru. Vrací počet skutečně změněných řádků. + +Měněné sloupce předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce: ```php -$count = $explorer->table('users') - ->where('id', 10) // musí se volat před update() +$affected = $explorer->table('users') + ->where('id', 10) ->update([ - 'name' => 'Ned Stark' + 'name' => 'John Smith', + 'year' => 1994, ]); -// UPDATE `users` SET `name`='Ned Stark' WHERE (`id` = 10) +// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Pro update můžeme využít operátorů `+=` a `-=`: +Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // všimněte si += + 'points+=' => 1, // zvýší hodnotu sloupce 'points' o 1 + 'coins-=' => 1, // sníží hodnotu sloupce 'coins' o 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Mazání záznamů (vrací počet smazaných řádků): + +Selection::delete(): int .[method] +---------------------------------- + +Maže řádky z tabulky podle zadaného filtru. Vrací počet smazaných řádků. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Při volání `update()` a `delete()` nezapomeňte pomocí `where()` specifikovat řádky, které se mají upravit/smazat. Pokud `where()` nepoužijete, operace se provede na celé tabulce! -Vazby mezi tabulkami -==================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- -Relace Has one --------------- -Relace has one je velmi běžná. Kniha *má jednoho* autora. Kniha *má jednoho* překladatele. Řádek, který je ve vztahu has one získáme pomocí metody `ref()`. Ta přijímá dva argumenty: jméno cílové tabulky a název spojovacího sloupce. Viz příklad: +Aktualizuje data v databázovém řádku reprezentovaném objektem `ActiveRow`. Jako parametr přijímá iterable s daty, která se mají aktualizovat (klíče jsou názvy sloupců). Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: + +Po provedení aktualizace se `ActiveRow` automaticky znovu načte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (např. triggery). Metoda vrací true pouze pokud došlo ke skutečné změně dat. ```php $book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$book->update([ + 'title' => 'Nový název knihy', +]); +echo $book->title; // Vypíše 'Nový název knihy' ``` -V příkladu výše vybíráme souvisejícího autora z tabulky `author`. Primární klíč tabulky `author` je hledán podle sloupce `book.author_id`. Metoda `ref()` vrací instanci `ActiveRow` nebo `null`, pokud hledaný záznam neexistuje. Vrácený řádek je instance `ActiveRow`, takže s ním můžeme pracovat stejně jako se záznamem knihy. +Tato metoda aktualizuje pouze jeden konkrétní řádek v databázi. Pro hromadnou aktualizaci více řádků použijte metodu [#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Smaže řádek z databáze, který je reprezentován objektem `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Smaže knihu s ID 1 +``` + +Tato metoda maže pouze jeden konkrétní řádek v databázi. Pro hromadné smazání více řádků použijte metodu [#Selection::delete()]. + + +Vazby mezi tabulkami +==================== + +V relačních databázích jsou data rozdělena do více tabulek a navzájem propojená pomocí cizích klíčů. Nette Database Explorer přináší revoluční způsob, jak s těmito vazbami pracovat - bez psaní JOIN dotazů a dokonce i bez znalosti názvů cizích klíčů. + +Pro ilustraci práce s vazbami použijeme příklad databáze knih ([najdete jej na GitHubu |https://github.com/nette-examples/books]). V databázi máme tabulky: + +- `author` - spisovatelé a překladatelé (sloupce `id`, `name`, `web`, `born`) +- `book` - knihy (sloupce `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - štítky (sloupce `id`, `name`) +- `book_tag` - vazební tabulka mezi knihami a štítky (sloupce `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Struktura databáze .<> + +V našem příkladu databáze knih můžeme najít několik typů vztahů (byť neodrážejí přesně realitu): + +- One-to-many (1:N) – každá kniha **má jednoho** autora, autor může napsat několik knih +- Zero-to-many (0:N) – kniha **může mít** překladatele, překladatel může přeložit několik knih +- Zero-to-one (0:1) – kniha **může mít** další díl +- Many-to-many (M:N) – kniha **může mít více** tagů a tag může být přiřazen několika knihám + +V těchto vztazích vždy existuje tabulka nadřazená a podřízená. Například ve vztahu mezi autorem a knihou je tabulka `author` nadřazená a `book` podřízená - můžeme si to představit tak, že kniha vždy "patří" nějakému autorovi. To se projevuje i ve struktuře databáze: podřízená tabulka `book` obsahuje cizí klíč `author_id`, který odkazuje na nadřazenou tabulku `author`. + +Potřebujeme-li vypsat knihy včetně jejich autorů, máme dvě možnosti. Buď data získáme jediným SQL dotazem pomocí JOIN: -// nebo přímo -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id +``` + +Nebo načteme data ve dvou krocích - nejprve knihy a pak jejich autory - a potom je v PHP poskládáme: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- ids autorů získaných knih ``` -Kniha má také jednoho překladatele, jeho jméno získáme snadno. +Druhý přístup je (možná překvapivě) efektivnější, protože načítá všechna data jen jednou a umožňuje lepší využití cache. A přesně takto funguje Nette Database Explorer, který tohle vše dělá pod kapotou a vám nabízí velmi příjemné API: + ```php -$book->ref('author', 'translator_id')->name +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'title: ' . $book->title; + echo 'written by: ' . $book->author->name; // $book->author je záznam z tabulky 'author' + echo 'translated by: ' . $book->translator?->name; +} ``` -Tento přístup je funkční, ale pořád trochu zbytečně těžkopádný, nemyslíte? Databáze už obsahuje definice cizích klíčů, tak proč je nepoužít automaticky. Pojďme to vyzkoušet. +Explorer automaticky generuje velice efektivní a rychlé SQL dotazy. Výše uvedený příklad provede pouze dva SELECT dotazy bez ohledu na počet vypisovaných knih. + + +Přístup k nadřazené tabulce +--------------------------- -Pokud přistoupíme k členské proměnné, která neexistuje, ActiveRow se pokusí použít jméno této proměnné pro relaci 'has one'. Čtení této proměnné je stejné jako volání metody `ref()` pouze s jedním parametrem. Tomuto parametru budeme říkat **klíč**. Tento klíč bude použit pro vyhledání cizího klíče v tabulce. Předaný klíč je porovnán se sloupci, a pokud odpovídá pravidlům, je cizí klíč na daném sloupci použit pro čtení dat z příbuzné tabulky. Viz příklad: +Přístup k nadřazené tabulce je velmi běžný. Kniha *má jednoho* autora. Kniha *může mít jednoho* překladatele. Řádek, který je ve vztahu získáme prostým přistoupením k property objektu ActiveRow, jejíž název odpovídá názvu sloupce s cizím klíčem bez `_id`: ```php -$book->author->name; -// je stejné jako -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // najde autora podle sloupce author_id +echo $book->translator?->name; // najde překladatele podle translator_id ``` -Instance ActiveRow nemá žádný sloupec `author`. Všechny sloupce tabulky `book` jsou prohledány na shodu s *klíčem*. Shoda v tomto případě znamená, že jméno sloupce musí obsahovat klíč. V příkladu výše sloupec `author_id` obsahuje řetězec 'author' a tedy odpovídá klíči 'author'. Pokud chceme přistoupit k záznamu překladatele, obdobným způsobem použijeme klíč 'translator', protože bude odpovídat sloupci `translator_id`. Více o logice párování klíčů si můžete přečíst v části [Joining expressions |#joining-key]. +Když přistoupíme k property `$book->author`, Explorer v tabulce `book` hledá sloupec, jehož název obsahuje řetězec `author` a končí na `_id` (tedy `author_id`). Podle hodnoty v tomto sloupci načte odpovídající záznam z tabulky `author`. Podobně funguje i `$book->translator`, který hledá sloupec `translator_id`. + +Operátor `?->` u překladatele používáme proto, že překladatel je volitelný - sloupec `translator_id` může obsahovat `null`. Použitím `?->` zabráníme chybě při pokusu o přístup k vlastnostem null hodnoty. Je to zkratka pro `$book->translator ? $book->translator->name : null`. + +Alternativní způsob je využít metodu `ref()`, která přijímá dva argumenty: název cílové tabulky a název spojovacího sloupce. Vrací instanci `ActiveRow` nebo `null`, pokud související záznam neexistuje. Vrácený řádek je instance `ActiveRow`, takže s ním můžeme pracovat stejně jako s původním záznamem: ```php -echo $book->title . ': '; -echo $book->author->name; -if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; -} +echo $book->ref('author', 'author_id')->name; // vazba na autora +echo $book->ref('author', 'translator_id')->name; // vazba na překladatele ``` +Používat `ref()` je užitečné ve chvíli, kdy v tabulce existuje sloupec `author`, takže přístup přes `$book->author` by nefungoval. Jinak je preferovaný způsob přes property. + Pokud chceme získat autora více knih, použijeme stejný přístup. Nette Database Explorer vybere z databáze záznamy autorů a překladatelů pro všechny knihy najednou. ```php @@ -471,13 +732,12 @@ $books = $explorer->table('book'); foreach ($books as $book) { echo $book->title . ': '; echo $book->author->name; - if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; - } + echo $book->translator?->name; } ``` Tento kód zavolá pouze tyto tři dotazy do databáze: + ```sql SELECT * FROM `book`; SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id ze sloupce author_id vybraných knih @@ -485,48 +745,51 @@ SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id ze sloupce translator_id ``` -Relace Has many ---------------- +Přístup k podřízené tabulce +--------------------------- -Relace 'has many' je pouze obrácená 'has one' relace. Autor napsal několik (*many*) knih. Autor přeložil několik (*many*) knih. Tento typ relace je obtížnější, protože vztah je pojmenovaný ('napsal', 'přeložil'). ActiveRow má metodu `related()`, která vrací pole souvisejících záznamů. Záznamy jsou opět instance ActiveRow. Viz příklad: +Přístup k podřízené tabulce je pouze obrácená než k nadřazené: Autor napsal několik knih. Autor přeložil několik knih. Tento typ relace je obtížnější, protože vztah je pojmenovaný ('napsal', 'přeložil'). ActiveRow má metodu `related()`, která vrací související záznamy. Záznamy jsou opět instance ActiveRow. Viz příklad: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' napsal:'; - +$author = $explorer->table('author')->get(1); +// Vypíše všechny knihy od autora foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Napsal: $book->title"; } - -echo 'a přeložil:'; +// Vypíše všechny knihy, které autor přeložil foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Přeložil: $book->title"; } ``` -Metoda `related()` přijímá popis spojení jako dva argumenty, nebo jako jeden argument spojený tečkou. První argument je cílová tabulka, druhý je sloupec. +Metoda `related()` přijímá popis spojení jako jeden argument s tečkovou notací nebo jako dva samostatné argumenty: + +```php +$author->related('book.translator_id'); // jeden argument +$author->related('book', 'translator_id'); // dva argumenty +``` + +Explorer dokáže automaticky detekovat správný spojovací sloupec podle názvu tabulky. Pokud explicitně neuvedeme spojovací sloupec, Explorer se pokusí najít cizí klíč, který odkazuje na aktuální tabulku. Například: ```php -$author->related('book.translator_id'); -// je stejné jako -$author->related('book', 'translator_id'); +$author->related('book'); // automaticky použije book.author_id ``` -Můžeme použít heuristiku Nette Database Explorer založenou na cizích klíčích a použít pouze **klíč**. Klíč bude porovnán s cizími klíči, které odkazují do aktuální tabulky (tabulka `author`). Pokud je nalezena shoda, Nette Database Explorer použije tento cizí klíč, v opačném případě vyhodí výjimku [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] nebo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Více o logice párování klíčů si můžete přečíst v části [Joining expressions |#joining-key]. +Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Metodu `related()` může samozřejmě volat na všechny získané autory a Nette Database Explorer načte všechny odpovídající knihy najednou. +Metodu `related()` můžeme samozřejmě použít i v cyklu a Explorer automaticky optimalizuje dotazy: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { echo $author->name . ' napsal:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Příklad uvedený výše spustí pouze tyto dva dotazy do databáze: +Tento kód vygeneruje pouze dva optimalizované SQL dotazy: ```sql SELECT * FROM `author`; @@ -534,18 +797,160 @@ SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů ``` +Vazba Many-to-many +------------------ + +U vazby many-to-many (M:N) potřebujeme nejprve získat záznamy z vazební tabulky (například `book_tag`). Přes ni se pak dostaneme k cílovým datům: + +```php +$book = $explorer->table('book')->get(1); +// vypíše názvy tagů přiřazených ke knize +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // vypíše název tagu přes vazební tabulku +} + +// nebo získáme tagy knihy z opačné strany: +$tag = $explorer->table('tag')->get(1); +// vypíše názvy knih označených tímto tagem +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; +} +``` + +Pro vazbu M:N je potřeba existence vazební tabulky (v našem případě `book_tag`), která obsahuje dva sloupce s cizími klíči (`book_id`, `tag_id`). Každý z těchto sloupců odkazuje na primární klíč jedné z propojovaných tabulek. + +Explorer opět optimalizuje SQL dotazy do efektivní podoby: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id vybraných knih +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id tagů nalezených v book_tag +``` + +Vyhledávat můžeme samozřejmě i opačným směrem, přes tagy ke knihám: + +```sql +SELECT * FROM `tag`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`tag_id` IN (1, 2, ...)); -- id vybraných tagů +SELECT * FROM `book` WHERE (`book`.`id` IN (1, 2, ...)); -- id knih nalezených v book_tag +``` + + +Dotazování přes související tabulky +----------------------------------- + +V metodách `where()`, `select()`, `order()` a `group()` můžeme používat speciální notace pro přístup k sloupcům z jiných tabulek. Explorer automaticky vytvoří potřebné JOINy. + +**Tečková notace** (`nadřazená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu podřízené tabulky: + +```php +$books = $explorer->table('book'); + +// Najde knihy, jejichž autor má jméno začínající na 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Seřadí knihy podle jména autora sestupně +$books->order('author.name DESC'); + +// Vypíše název knihy a jméno autora +$books->select('book.title, author.name'); +``` + +**Dvojtečková notace** (`:podřízená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu nadřazené tabulky: + +```php +$authors = $explorer->table('author'); + +// Najde autory, kteří napsali knihu s 'PHP' v názvu +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Spočítá počet knih pro každého autora +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Ve výše uvedeném příkladu s dvojtečkovou notací (`:book.title`) není specifikován sloupec s cizím klíčem. Explorer automaticky detekuje správný sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`. Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Pro složitější dotazy můžeme spojovací sloupec explicitně uvést v závorce: + +```php +// Najde autory, kteří přeložili knihu s 'PHP' v názvu +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Notace lze řetězit pro přístup přes více tabulek: + +```php +// Najde autory knih označených tagem 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + +Pro rozšíření podmínek u spojování tabulek (podmínek za klíčovým slovem ON v SQL) slouží metoda `joinWhere()`: + +```php +// Najde knihy přeložené překladatelem jménem 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id +// AND (translator.name = 'David') +``` + +Pro složitější dotazy s více JOINy můžeme definovat aliasy tabulek: + +```php +$tags = $explorer->table('tag') + ->joinWhere(':book_tag:book:author', 'book_author.born < ?', 1950) + ->alias(':book_tag:book:author', 'book_author'); +``` + + +Rozšíření podmínek pro JOIN +--------------------------- + +Metoda `joinWhere()` rozšiřuje podmínky, které se uvádějí při propojování tabulek v SQL za klíčovým slovem `ON`. + +Dejme tomu, že chceme najít knihy přeložené konkrétním překladatelem: + +```php +// Najde knihy přeložené překladatelem jménem 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +V podmínce `joinWhere()` můžeme používat stejné konstrukce jako v metodě `where()` - operátory, zástupné otazníky, pole hodnot či SQL výrazy. + +Pro složitější dotazy s více JOINy můžeme definovat aliasy tabulek: + +```php +$tags = $explorer->table('tag') + ->joinWhere(':book_tag:book:author', 'book_author.born < ?', 1950) + ->alias(':book_tag:book:author', 'book_author'); +// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` +// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` +// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` +// AND (`book_author`.`born` < 1950) +``` + +Všimněte si, že zatímco metoda `where()` přidává podmínky do klauzule `WHERE`, metoda `joinWhere()` rozšiřuje podmínky v klauzuli `ON` při spojování tabulek. + + Ruční vytvoření Explorer ======================== -Pokud jsme si vytvořili databázové spojení pomocí aplikační konfigurace, nemusíme se o nic starat. Vytvořila se nám totiž i služba typu `Nette\Database\Explorer`, kterou si můžeme předat pomocí DI. - -Pokud ale používáme Nette Database Explorer samostatně, musíme instanci `Nette\Database\Explorer` vytvořit ručně. +Pokud nepoužíváte Nette DI kontejner, můžete instanci `Nette\Database\Explorer` vytvořit ručně: ```php -// $storage obsahuje implementaci Nette\Caching\Storage, např.: -$storage = new Nette\Caching\Storages\FileStorage($tempDir); -$connection = new Nette\Database\Connection($dsn, $user, $password); -$structure = new Nette\Database\Structure($connection, $storage); -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); +use Nette\Database; + +// $storage implementuje Nette\Caching\Storage, např.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// připojení k databázi +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// stará se o reflexi databázové struktury +$structure = new Database\Structure($connection, $storage); +// nebo jiná implementace rozhraní Nette\Database\Conventions; definuje pravidla pro mapování názvů tabulek, sloupců a cizích klíčů +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/cs/reflection.texy b/database/cs/reflection.texy new file mode 100644 index 0000000000..04dc9ca7a6 --- /dev/null +++ b/database/cs/reflection.texy @@ -0,0 +1,170 @@ +Reflexe +******* + +Nette Database poskytuje nástroje pro introspekci databázové struktury pomocí třídy [api:Nette\Database\Reflection\Reflection]. Ta umožňuje získávat informace o tabulkách, sloupcích, indexech a cizích klíčích. Reflexi můžete využít ke generování schémat, vytváření flexibilních aplikací pracujících s databází nebo obecných databázových nástrojů. + +Objekt reflexe získáme z instance připojení k databázi: + +```php +$reflection = $connection->getReflection(); +``` + + +Práce s tabulkami +================= + +Pomocí reflexe můžeme procházet všechny tabulky v databázi: + + +getTables(): array .[method] +---------------------------- +Vrací asocitivní pole, kde klíčem je název tabulky a hodnotou pole s metadaty tabulky. + +```php +// Výpis názvů všech tabulek +foreach ($reflection->getTables() as $table) { + echo $table['name'] . "\n"; +} +``` + + +hasTable(string $name): bool .[method] +-------------------------------------- +Vrací `true`, pokud tabulka existuje, jinak `false`. + +```php +// Ověření existence tabulky +if ($reflection->hasTable('users')) { + echo "Tabulka users existuje"; +} +``` + + +getTable(string $name): Nette\Database\Reflection\Table .[method] +----------------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\Table` reprezentující danou tabulku. Pokud tabulka neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingTableException`. + +```php +// Získání konkrétní tabulky +$table = $reflection->getTable('users'); +``` + + +Informace o sloupcích +===================== + +Objekt `Nette\Database\Reflection\Table`, který získáme voláním `getTable()`, nám umožňuje získat detailní informace o sloupcích tabulky. + + +getColumns(): array .[method] +----------------------------- +Vrací pole objektů `Nette\Database\Reflection\Column` reprezentujících sloupce tabulky. + + +getColumn(string $name): Nette\Database\Reflection\Column .[method] +------------------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\Column` reprezentující daný sloupec. Pokud sloupec neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingColumnException`. + +Objekt `Column` poskytuje tyto vlastnosti: + +- `name`: Název sloupce. +- `nativeType`: Datový typ sloupce specifický pro danou databázi. +- `type`: Normalizovaný datový typ sloupce (viz konstanty `Nette\Utils\Type`). +- `nullable`: `true`, pokud sloupec může obsahovat hodnotu `NULL`, jinak `false`. +- `primary`: `true`, pokud je sloupec součástí primárního klíče, jinak `false`. +- `autoIncrement`: `true`, pokud je sloupec auto-increment, jinak `false`. +- `default`: Výchozí hodnota sloupce, nebo `null`, pokud není definována. +- `vendor`: Pole s dalšími informacemi specifickými pro danou databázi. + +```php +// Procházení všech sloupců tabulky users +$table = $reflection->getTable('users'); +foreach ($table->getColumns() as $column) { + echo "Sloupec: " . $column->name . "\n"; + echo "Typ: " . $column->nativeType . "\n"; + echo "Může být NULL: " . ($column->nullable ? 'Ano' : 'Ne') . "\n"; + echo "Výchozí hodnota: " . ($column->default ?? 'Není') . "\n"; + echo "Je primární klíč: " . ($column->primary ? 'Ano' : 'Ne') . "\n"; + echo "Je auto-increment: " . ($column->autoIncrement ? 'Ano' : 'Ne') . "\n"; +} + +// Získání konkrétního sloupce +$idColumn = $table->getColumn('id'); +``` + + +Indexy a primární klíče +======================= + + +getIndexes(): array .[method] +----------------------------- +Vrací pole objektů `Nette\Database\Reflection\Index` reprezentujících indexy tabulky. + + +getIndex(string $name): Nette\Database\Reflection\Index .[method] +----------------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\Index` reprezentující daný index. Pokud index neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingIndexException`. + + +getPrimaryKey(): ?Nette\Database\Reflection\Index .[method] +----------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\Index` reprezentující primární klíč tabulky, nebo `null`, pokud tabulka nemá primární klíč. + +Objekt `Index` poskytuje tyto vlastnosti: + +- `name`: Název indexu. +- `columns`: Pole objektů `Nette\Database\Reflection\Column` reprezentujících sloupce, které jsou součástí indexu. +- `unique`: `true`, pokud je index unikátní, jinak `false`. +- `primary`: `true`, pokud je index primárním klíčem, jinak `false`. + +```php +$table = $reflection->getTable('users'); + +$vypisNazvySloupcu = fn(array $columns) => implode(', ', array_map(fn($col) => $col->name, $columns)); + +// Výpis všech indexů +foreach ($table->getIndexes() as $index) { + echo "Index: " . ($index->name ?? 'Nepojmenovaný') . "\n"; + echo "Sloupce: " . $vypisNazvySloupcu($index->columns) . "\n"; + echo "Je unikátní: " . ($index->unique ? 'Ano' : 'Ne') . "\n"; + echo "Je primární klíč: " . ($index->primary ? 'Ano' : 'Ne') . "\n"; +} + +// Získání primárního klíče +if ($primaryKey = $table->getPrimaryKey()) { + echo "Primární klíč: " . $vypisNazvySloupcu($primaryKey->columns) . "\n"; +} +``` + + +Cizí klíče +========== + + +getForeignKeys(): array .[method] +--------------------------------- +Vrací pole objektů `Nette\Database\Reflection\ForeignKey` reprezentujících cizí klíče tabulky. + + +getForeignKey(string $name): Nette\Database\Reflection\ForeignKey .[method] +--------------------------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\ForeignKey` reprezentující daný cizí klíč. Pokud cizí klíč neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingForeignKeyException`. + +Objekt `ForeignKey` poskytuje tyto vlastnosti: + +- `name`: Název cizího klíče. +- `localColumns`: Pole objektů `Nette\Database\Reflection\Column` reprezentujících lokální sloupce, které tvoří cizí klíč. +- `foreignTable`: Objekt `Nette\Database\Reflection\Table` reprezentující cizí tabulku, na kterou cizí klíč odkazuje. +- `foreignColumns`: Pole objektů `Nette\Database\Reflection\Column` reprezentujících cizí sloupce, na které cizí klíč odkazuje. + +```php +$table = $reflection->getTable('books'); + +foreach ($table->getForeignKeys() as $fk) { + echo "Cizí klíč: " . ($fk->name ?? 'Nepojmenovaný') . "\n"; + echo "Lokální sloupce: " . $vypisNazvySloupcu($fk->localColumns) . "\n"; + echo "Odkazuje na tabulku: {$fk->foreignTable->name}\n"; + echo "Odkazuje na sloupce: " . $vypisNazvySloupcu($fk->foreignColumns) . "\n"; +} +``` diff --git a/database/files/db-schema-1-.webp b/database/files/db-schema-1-.webp index 29905706c0..6bd9b0598d 100644 Binary files a/database/files/db-schema-1-.webp and b/database/files/db-schema-1-.webp differ