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