From 2cfbe83eaee05a9fee004a6edec8e40870ab847a Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 16 Dec 2024 02:03:38 +0100 Subject: [PATCH] wip --- database/cs/explorer.texy | 935 ++++++++++++++++++++++--------- database/cs/reflection.texy | 170 ++++++ database/files/db-schema-1-.webp | Bin 10492 -> 41462 bytes 3 files changed, 836 insertions(+), 269 deletions(-) create mode 100644 database/cs/reflection.texy diff --git a/database/cs/explorer.texy b/database/cs/explorer.texy index d1f397fd5a..ff0bbaced1 100644 --- a/database/cs/explorer.texy +++ b/database/cs/explorer.texy @@ -3,530 +3,795 @@ 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 +- Funguje okamžitě bez jakékoliv konfigurace či generování entit
-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], +]); +``` + +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: -// toto způsobí výjimku, tato syntax není podporovaná -$table->where('NOT id ?', $ids); +```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: -Příklady použití: +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` + +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() ------- -Alternativní způsob pro nastavení limitu a offsetu: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +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 -Příklady použití: +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 +-------------------------------------- + +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ě. -Ř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`. +fetch(): ?ActiveRow .[method] +----------------------------- + +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`: -Abychom mohli spojovat přes `translator_id`, stačí přidat volitelný parametr do spojovacího výrazu. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +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' => ..., ...] +``` -->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Ě +Agregace +======== -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // ŠPATNĚ! hodnoty dosazujeme přes parametr -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // SPRÁVNĚ +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()` + + +count(string $expr): int .[method] +---------------------------------- + +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] +----------------------------------------------- + +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: -| `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 +```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 parametry můžeme předávat i soubory nebo objekty DateTime: +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); +``` + +**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] +------------------------------------------------- + +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 `-=`: -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: +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 nutnosti cokoliv konfigurovat nebo generovat. + +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`) -// nebo přímo -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +[* db-schema-1-.webp *] *** Struktura databáze .<> + +V našem příkladu databáze knih najdeme několik typů vztahů (byť model je zjednodušený oproti realitě): + +- 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 několik** 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ě jmen jejich autorů, máme dvě možnosti. Buď data získáme jediným SQL dotazem pomocí JOIN: + +```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. -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 +--------------------------- + +Přístup k nadřazené tabulce je přímočarý. Jde o vztahy jako *kniha má autora* nebo *kniha může mít překladatele*. Související záznam získáme přes property objektu ActiveRow - její název odpovídá názvu sloupce s cizím klíčem bez přípony `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` (tedy `author_id`). Podle hodnoty v tomto sloupci načte odpovídající záznam z tabulky `author`. Podobně funguje i `$book->translator`, který využije sloupec `translator_id`. Protože překladatel je volitelný a sloupec `translator_id` může obsahovat `null`, použijeme v kódu operátor `?->`. + +Alternativní cestu nabízí metoda `ref()`, která přijímá dva argumenty, název cílové tabulky a název spojovacího sloupce, a vrací instanci `ActiveRow` nebo `null`: ```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 ``` -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. +Metoda `ref()` se hodí, pokud nelze použít přístup přes property, protože tabulka obsahuje sloupec se stejným názvem (tj. `author`). V ostatních případech je doporučeno používat přístup přes property, který je čitelnější. + +Explorer automaticky optimalizuje databázové dotazy. Když procházíme knihy v cyklu a přistupujeme k jejich souvisejícím záznamům (autorům, překladatelům), Explorer negeneruje dotaz pro každou knihu zvlášť. Místo toho provede pouze jeden SELECT pro každý typ vazby, čímž výrazně snižuje zátěž databáze. Například: ```php $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: +Tento kód zavolá pouze tyto tři bleskové 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 SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id ze sloupce translator_id vybraných knih ``` +.[note] +Logika dohledávání spojovacího sloupce 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 s existujícími vztahy mezi tabulkami. -Relace Has many ---------------- -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 +--------------------------- + +Přístup k podřízené tabulce funguje v opačném směru. Nyní se ptáme *jaké knihy napsal tento autor?* nebo *přeložil tento překladatel?*. Pro tento typ dotazu používáme metodu `related()`, která `Selection` se souvisejícími záznamy. Podívejme se na 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'); -// je stejné jako -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // jeden argument +$author->related('book', 'translator_id'); // dva argumenty ``` -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]. +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 a obsahuje její název. Například: + +```php +$author->related('book'); // automaticky použije book.author_id +``` -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. +Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Metodu `related()` můžeme samozřejmě použít i při procházení více záznamů v cyklu a Explorer i v tomto případě 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 bleskové SQL dotazy: ```sql SELECT * FROM `author`; @@ -534,18 +799,150 @@ SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů ``` +Vazba Many-to-many +------------------ + +Pro vazbu many-to-many (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. +Nejprve získáme záznamy z vazební tabulky pomocí `related('book_tag')` a dále pokračujeme 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 +} + +$tag = $explorer->table('tag')->get(1); +// nebo opačně: vypíše názvy knih označených tímto tagem +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; +} +``` + +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 +``` + + +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 29905706c0c7e486caa820c4de7a3f206cfdd45a..6bd9b0598dd6206f074f014caa226694c136c167 100644 GIT binary patch literal 41462 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