diff --git a/database/bg/@left-menu.texy b/database/bg/@left-menu.texy index fdb4f56e48..f2eb17b5e5 100644 --- a/database/bg/@left-menu.texy +++ b/database/bg/@left-menu.texy @@ -4,3 +4,4 @@ - [Изследовател |Explorer] - [Размисъл |Reflection] - [Настройване |configuration] +- [Рискове за сигурността |security] diff --git a/database/bg/explorer.texy b/database/bg/explorer.texy index c19b98d0e6..494de7790a 100644 --- a/database/bg/explorer.texy +++ b/database/bg/explorer.texy @@ -3,548 +3,927 @@
-Nette Database Explorer улеснява много извличането на данни от база данни, без да се налага да се пишат SQL заявки. +Nette Database Explorer е мощен слой, който значително опростява извличането на данни от базата данни, без да е необходимо да се пишат SQL заявки. -- използва ефективни заявки -- данните не се прехвърлят ненужно. -- предлага елегантен синтаксис +- Работата с данни е естествена и лесна за разбиране +- Генерира оптимизирани SQL заявки, които извличат само необходимите данни +- Осигурява лесен достъп до свързани данни, без да е необходимо да се пишат JOIN заявки +- Работи незабавно без конфигуриране или генериране на ентитети
-За да използвате Database Explorer, започнете с таблица - извикайте `table()` на обекта [api:Nette\Database\Explorer]. Най-лесният начин за получаване на екземпляр на обекта на контекста е [описан тук |core#Connection-and-Configuration] или, в случай че Nette Database Explorer се използва като отделен инструмент, той може да бъде [създаден ръчно |#Creating-Explorer-Manually]. +Nette Database Explorer е разширение на слоя [Nette Database Core |core] от ниско ниво, което добавя удобен обектно-ориентиран подход към управлението на бази данни. + +Работата с Explorer започва с извикване на метода `table()` на обекта [api:Nette\Database\Explorer] (как да го получите е [описано тук |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // името на таблицата в базата данни е 'book' +$books = $explorer->table('book'); // 'book' е името на таблицата ``` -Извикването връща екземпляр на обекта [Selection |api:Nette\Database\Table\Selection], който може да бъде итериран, за да се извлекат всички книги. Всеки елемент (ред) е представен от екземпляр на [ActiveRow |api:Nette\Database\Table\ActiveRow] с данните, показани в неговите свойства: +Методът връща обект [Selection |api:Nette\Database\Table\Selection], който представлява SQL заявка. Към този обект могат да бъдат верижно свързани допълнителни методи за филтриране и сортиране на резултатите. Запитването се събира и изпълнява само когато се изискват данни, например чрез итерация с `foreach`. Всеки ред се представя от обект [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // извежда колона 'title' + echo $book->author_id; // извежда колона 'author_id' } ``` -Извличането само на един конкретен ред се извършва чрез метода `get()`, който директно връща инстанция на ActiveRow. +Explorer значително опростява работата с [връзките между таблиците |#Vazby mezi tabulkami]. Следващият пример показва колко лесно можем да изведем данни от свързани таблици (книги и техните автори). Обърнете внимание, че не е необходимо да се пишат заявки JOIN; Nette ги генерира вместо нас: ```php -$book = $explorer->table('book')->get(2); // връща книга с ID 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // създава JOIN към таблицата "author". +} ``` -Нека разгледаме един често срещан случай на употреба. Трябва да се запознаете с книгите и техните автори. Това е обичайното съотношение 1:N. Често използвано решение е извличането на данните чрез една SQL заявка с обединяване на таблици. Вторият вариант е да получите данните поотделно, да стартирате една заявка, за да получите книгите, и след това да получите автора за всяка книга с друга заявка (напр. в цикъл foreach). Това може лесно да се оптимизира, за да се изпълняват само две заявки - една за книгите и една за желаните автори - и точно това прави Nette Database Explorer. +Nette Database Explorer оптимизира заявките за максимална ефективност. В горния пример се изпълняват само две SELECT заявки, независимо дали обработваме 10 или 10 000 книги. -В примерите по-долу ще работим със схемата на базата данни, показана на фигурата. Съществуват връзки OneHasMany (1:N) (авторът на книгата `author_id` и евентуален преводач `translator_id`, който може да бъде `null`) и ManyHasMany (M:N) между книгата и нейните етикети. +Освен това Explorer проследява кои колони се използват в кода и извлича само тях от базата данни, като по този начин спестява допълнително производителност. Това поведение е напълно автоматично и адаптивно. Ако по-късно промените кода, за да използвате допълнителни колони, Explorer автоматично коригира заявките. Не е необходимо да конфигурирате каквото и да било или да мислите кои колони ще бъдат необходими - оставете това на Nette. -[Пример, включващ схема, може да бъде намерен в GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Структура на базата данни, използвана в примерите .<> +Филтриране и сортиране .[#toc-filtering-and-sorting] +==================================================== -Следващият код изброява името на автора за всяка книга и всички нейни етикети. [По-долу ще разгледаме |#Working-with-Relationships] как работи това вътрешно. +Класът `Selection` предоставя методи за филтриране и сортиране на данни. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Добавя условие WHERE. Няколко условия се комбинират с помощта на AND | +| `whereOr(array $conditions)` | Добавя група от условия WHERE, комбинирани с помощта на OR | +| `wherePrimary($value)` | Добавя условие WHERE въз основа на първичния ключ | +| `order($columns, ...$params)` | Задава сортиране с ORDER BY | +| `select($columns, ...$params)` | Определя кои колони да се извличат | +| `limit($limit, $offset = null)` | Ограничава броя на редовете (LIMIT) и по избор задава OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Задава странициране | +| `group($columns, ...$params)` | Групира редове (GROUP BY) | +| `having($condition, ...$params)`| Добавя условие HAVING за филтриране на групираните редове | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'написано от: ' . $book->author->name; // $book->author е низ от таблицата 'author' +Методите могат да бъдат верижно свързани (т.нар. [плавен интерфейс |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 е низ от таблицата 'tag' - } -} -``` +Тези методи също така позволяват използването на специални обозначения за достъп до [данни от свързани таблици |#Dotazování přes související tabulky]. -Ще останете доволни от ефективността на работата на слоя с бази данни. Примерът по-горе прави постоянен брой заявки, които изглеждат по следния начин -```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)) -``` +Избягване и идентификатори .[#toc-escaping-and-identifiers] +----------------------------------------------------------- -Ако използвате [кеша |caching:] (разрешен по подразбиране), няма да се правят ненужни заявки за колони. След първата заявка имената на използваните колони ще се съхранят в кеша и Nette Database Explorer ще изпълнява само заявки с необходимите колони: +Методите автоматично есквапират параметрите и цитират идентификаторите (имена на таблици и колони), като предотвратяват SQL инжекции. За да се гарантира правилното функциониране, трябва да се спазват няколко правила: -```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)) +- Записвайте ключови думи, имена на функции, процедури и т.н. с **uppercase**. +- Записвайте имената на колони и таблици с **малки букви**. +- Винаги предавайте низове с помощта на **параметри**. + +```php +where('name = ' . $name); // **DISASTER**: уязвим към SQL инжектиране +where('name LIKE "%search%"'); // **WRONG**: усложнява автоматичното цитиране +where('name LIKE ?', '%search%'); // **CORRECT**: стойност, предадена като параметър + +where('name like ?', $name); // **WRONG**: генерира: `име` `като` ? +where('name LIKE ?', $name); // **CORRECT**: генерира: `име` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`име`) = ? ``` -Избори .[#toc-selections] -========================= +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Вижте опции за филтриране и ограничения на низове [api:Nette\Database\Table\Selection]: +Филтриране на резултатите чрез условията WHERE. Силата му се състои в интелигентната обработка на различни типове стойности и автоматичния избор на SQL оператори. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Задайте WHERE, като използвате AND като свързващо звено, ако са зададени две или повече условия -| `$table->whereOr($where)` | Задайте WHERE, като използвате OR като свързващо звено, ако са зададени две или повече условия -| `$table->order($columns)` | Задайте ORDER BY, например като използвате израза `('column DESC, id DESC')`. -| `$table->select($columns)` | Задайте извлечените колони, например с помощта на израза `('col, MD5(col) AS hash')`. -| `$table->limit($limit[, $offset])` | Задаване на LIMIT и OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | включване на странирането. -| `$table->group($columns)` | set GROUP BY -| `$table->having($having)` | set HAVING +Основна употреба: -Можем да използваме т.нар. [флуентен интерфейс |nette:introduction-to-object-oriented-programming#fluent-interfaces], например `$table->where(...)->order(...)->limit(...)`. Няколко условия `where` или `whereOr` се свързват чрез оператора `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 +``` +Благодарение на автоматичното разпознаване на подходящи оператори не е необходимо да се занимавате със специални случаи - Nette се справя с тях вместо вас: -където() .[#toc-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) +// Заместващият знак ? може да се използва без оператор: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer може автоматично да добави необходимите оператори за предадените стойности: +Методът се справя правилно и с отрицателни условия и празни масиви: -.[language-php] -| `$table->where('field', $value)` | поле = $стойност -| `$table->where('field', null)` | полето е NULL -| `$table->where('field > ?', $val)` | поле > $стойност -| `$table->where('field', [1, 2])` | поле IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 ИЛИ име = "Джон Сноу -| `$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 -- не намира нищо +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- намира всичко +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- намира всичко +// $table->where('NOT id ?', $ids); // ПРЕДУПРЕЖДЕНИЕ: Този синтаксис не се поддържа +``` -Можете да посочите заместител дори без оператора за колони. Тези обаждания са едни и същи. +Можете също така да подадете резултата от друга заявка за таблица като параметър, създавайки подзаявка: ```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')); ``` -Тази функция ви позволява да генерирате правилния оператор въз основа на стойността: +Условията могат да се предават и като масив, като елементите се комбинират с помощта на 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', +]); ``` -Изборът обработва правилно и отрицателни условия и работи за празни масиви: +В масива могат да се използват двойки ключ-стойност, като Nette отново ще избере автоматично правилните оператори: ```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], +]); +``` -// това ще доведе до изключение, този синтаксис не се поддържа -$table->where('NOT id ?', $ids); +Можем също така да смесваме SQL изрази със заместители и множество параметри. Това е полезно за сложни условия с точно определени оператори: + +```php +// WHERE (`възраст` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // два параметъра се предават като масив +]); ``` +Многократните извиквания на `where()` автоматично комбинират условията с помощта на AND. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Пример за използване без параметри: +Подобно на `where()`, но комбинира условия с помощта на 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, ]); ``` -Използваме параметри. Ако не посочите оператор, Nette Database Explorer автоматично ще добави подходящия оператор: +Могат да се използват и по-сложни изрази: ```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, ]); ``` -Ключът може да съдържа израз, съдържащ заместващи символи, и след това да предаде параметрите в стойността: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Добавя условие за първичния ключ на таблицата: + +```php +// WHERE `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Ако таблицата има съставен първичен ключ (напр. `foo_id`, `bar_id`), го предаваме като масив: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// 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(); ``` -поръчка() .[#toc-order] ------------------------ +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Примери за употреба: +Посочва реда, в който се връщат редовете. Можете да сортирате по една или повече колони, във възходящ или низходящ ред, или по потребителски израз: ```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 ``` -изберете() .[#toc-select] -------------------------- +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Примери за употреба: +Посочва колоните, които да бъдат върнати от базата данни. По подразбиране Nette Database Explorer връща само колоните, които действително се използват в кода. Използвайте метода `select()`, когато трябва да извлечете конкретни изрази: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +След това псевдонимите, дефинирани с помощта на `AS`, са достъпни като свойства на обекта `ActiveRow`: -ограничение() .[#toc-limit] ---------------------------- +```php +foreach ($table as $row) { + echo $row->formatted_date; // достъп до псевдонима +} +``` + + +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Примери за употреба: +Ограничава броя на върнатите редове (LIMIT) и по желание задава отместване: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (връща първите 10 реда) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +За страниране е по-подходящо да се използва методът `page()`. -страница() .[#toc-page] ------------------------ -Алтернативен начин за задаване на границата и отместването: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Опростява странирането на резултатите. Приема номера на страницата (започвайки от 1) и броя на елементите на страница. По желание можете да подадете препратка към променлива, в която ще се съхранява общият брой страници: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Получаване на последния номер на страница, предаден на променливата `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Групира редове по зададените колони (GROUP BY). Обикновено се използва в комбинация с функции за агрегиране: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Отчита броя на продуктите във всяка категория +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -група() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Примери за употреба: +Задава условие за филтриране на групирани редове (HAVING). Може да се използва в комбинация с метода `group()` и функциите за агрегиране: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Намира категории с повече от 100 продукта +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -като() .[#toc-having] ---------------------- +Четене на данни +=============== + +За четене на данни от базата данни са налични няколко полезни метода: + +.[language-php] +| `foreach ($table as $key => $row)` | Итервюира всички редове, `$key` е стойността на първичния ключ, `$row` е обект ActiveRow | +| `$row = $table->get($key)` | Връща един ред по първичен ключ | +| `$row = $table->fetch()` | Връща текущия ред и придвижва указателя към следващия | +| `$array = $table->fetchPairs()` | Създава асоциативен масив от резултатите | +| `$array = $table->fetchAll()` | Връща всички редове като масив | +| `count($table)` | Връща броя на редовете в обекта Selection | + +Обектът [ActiveRow |api:Nette\Database\Table\ActiveRow] е само за четене. Това означава, че не можете да променяте стойностите на неговите свойства. Това ограничение осигурява последователност на данните и предотвратява неочаквани странични ефекти. Данните се извличат от базата данни и всякакви промени трябва да се правят изрично и по контролиран начин. + -Примери за употреба: +`foreach` - Итериране през всички редове +---------------------------------------- + +Най-лесният начин за изпълнение на заявка и извличане на редове е чрез итерация с цикъла `foreach`. Той автоматично изпълнява SQL заявката. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = първичен ключ, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Филтриране по стойност на друга таблица .[#toc-joining-key] ------------------------------------------------------------ +get($key): ?ActiveRow .[method] +------------------------------- + +Изпълнява SQL заявка и връща ред по първичния му ключ или `null`, ако той не съществува. + +```php +$book = $explorer->table('book')->get(123); // връща ActiveRow с ID 123 или null +if ($book) { + echo $book->title; +} +``` -Много често искате да филтрирате резултатите по някакво условие, което включва друга таблица в базата данни. Такива условия изискват обединяване на таблици. Въпреки това вече не е необходимо да ги пишете. -Например, да предположим, че искате да получите всички книги, чието име на автор е "Jon". Необходимо е да напишете само ключа за присъединяване на връзката и името на колоната в таблицата за присъединяване. Ключът за обединяване се взема от колоната, която се отнася до таблицата, която искате да обедините. В нашия пример (вж. схемата на db) това е колоната `author_id`, като е достатъчно да се използва само първата ѝ част - `author` (суфиксът `_id` може да се пропусне). `name` - е колоната в таблицата `author`, която искаме да използваме. Условието за преводач на книги (което е свързано с колоната `translator_id`) може да бъде създадено също толкова лесно. +fetch(): ?ActiveRow .[method] +----------------------------- + +Връща един ред и придвижва вътрешния указател към следващия. Ако няма повече редове, се връща `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); +} ``` -Логиката на свързващите ключове се определя от прилагането на [Conventions |api:Nette\Database\Conventions]. Препоръчваме ви да използвате [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], която анализира чуждите ключове и улеснява работата с тези връзки. -Връзката между една книга и нейния автор е 1:N. Възможна е и обратна зависимост. Наричаме това **обратна връзка**. Вижте друг пример. Искаме да привлечем всички автори, които са написали повече от 3 книги. За да направим връзката обратна, използваме `:` (двоеточие). Двоеточие означает, что объединенное отношение имеет значение hasMany (и это вполне логично, так как две точки больше, чем одна). К сожалению, класс Selection недостаточно умен, поэтому мы должны помочь с агрегацией и предоставить оператор `GROUP BY`, също така условието трябва да се запише като оператор `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Връща резултатите като асоциативен масив. Първият аргумент указва името на колоната, което ще се използва като ключ в масива, а вторият аргумент указва името на колоната, което ще се използва като стойност: ```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", ...] +``` + +Ако е посочен само ключът на колоната, стойността ще бъде целият ред, т.е. обектът `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Може би сте забелязали, че декларацията за присъединяване се отнася до книга, но не е ясно дали се присъединяваме чрез `author_id` или `translator_id`. В горния пример Selection се присъединява чрез колоната `author_id`, тъй като има съвпадение с оригиналната таблица, таблицата `author`. Ако няма такова съвпадение и има повече възможности, Nette ще хвърли [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Ако като ключ е посочен `null`, масивът ще бъде цифрово индексиран, започвайки от нула: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => "John Doe", 1 => "Jane Doe", ...] +``` -За да извършите сливането чрез колона `translator_id`, въведете незадължителен параметър в израза за сливане. +Можете също така да подадете обратно извикване като параметър, което ще върне или самата стойност, или двойка ключ-стойност за всеки ред. Ако обратното извикване върне само стойност, ключът ще бъде първичният ключ на реда: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Първа книга (Ян Новак)', ...] + +// Обратното извикване може също да върне масив с двойка ключ и стойност: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ["Първа книга" => "Ян Новак", ...] ``` -Нека разгледаме един по-сложен израз за обединяване. -Искаме да намерим всички автори, които са написали нещо за PHP. Всички книги имат тагове, така че трябва да изберем онези автори, които са написали някоя книга с таг PHP. +fetchAll(): array .[method] +--------------------------- + +Връща всички редове като асоциативен масив от обекти `ActiveRow`, където ключовете са стойностите на първичния ключ. ```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, ...), ...] ``` -Обобщени заявки .[#toc-aggregate-queries] ------------------------------------------ +count(): int .[method] +---------------------- -| `$table->count('*')` | получаване на брой редове -| `$table->count("DISTINCT $column")` | получаване на броя на отделните стойности -| `$table->min($column)` | получаване на минималната стойност -| `$table->max($column)` | получаване на максималната стойност -| `$table->sum($column)` | получаване на сумата от всички стойности -| `$table->aggregation("GROUP_CONCAT($column)")` | Изпълнение на всяка функция за агрегиране +Методът `count()` без параметри връща броя на редовете в обекта `Selection`: -.[caution] -Методът `count()` без посочване на параметри избира всички записи и връща размера на масива, което е много неефективно. Например, ако трябва да изчислите броя на редовете за страниране, винаги посочвайте първия аргумент. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // алтернатива +``` +Забележка: Методът `count()` с параметър изпълнява функцията за агрегиране COUNT в базата данни, както е описано по-долу. -Стенография и цитати .[#toc-escaping-quoting] -============================================= -Database Explorer е интелигентен и ще премахне параметрите и идентификаторите на цитати вместо вас. Трябва обаче да се спазват следните основни правила: +ActiveRow::toArray(): array .[method] +------------------------------------- -- ключовите думи, функциите и процедурите трябва да са с главни букви -- колони и таблици с малки букви -- предавайте променливи като параметри, не ги конкатенирайте. +Преобразува обекта `ActiveRow` в асоциативен масив, в който ключовете са имена на колони, а стойностите - съответните данни. ```php -->where('name like ?', 'John'); // НЕПРАВИЛЬНО! Генерирует: `name` `like` ? -->where('name LIKE ?', 'John'); // ПРАВИЛЬНО +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray ще бъде ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` -->where('KEY = ?', $value); // НЕПРАВИЛЬНО! КЛЮЧ - это ключевое слово -->where('key = ?', $value); // ПРАВИЛЬНО. Генерирует: `key` = ? -->where('name = ' . $name); // Неправильно! sql-инъекция! -->where('name = ?', $name); // ПРАВИЛЬНО +Агрегиране .[#toc-aggregation] +============================== -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // НЕПРАВИЛЬНО! Передавайте переменные как параметры, не конкатенируйте -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // ПРАВИЛЬНО +Класът `Selection` предоставя методи за лесно изпълнение на функции за агрегиране (COUNT, SUM, MIN, MAX, AVG и др.). + +.[language-php] +| `count($expr)` | Преброява броя на редовете | +| `min($expr)` | Връща минималната стойност в дадена колона | +| `max($expr)` | Връща максималната стойност в дадена колона | +| `sum($expr)` | Връща сумата от стойностите в дадена колона | +| `aggregation($function)` | Позволява всякаква функция за агрегиране, като например `AVG()` или `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Изпълнява SQL заявка с функцията COUNT и връща резултата. Този метод се използва, за да се определи колко реда отговарят на определено условие: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Забележка: [count() |#count()] без параметър просто връща броя на редовете в обекта `Selection`. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Методите `min()` и `max()` връщат минималните и максималните стойности в посочената колона или израз: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); ``` -.[warning] -Неправилната употреба може да доведе до пропуски в сигурността +sum(string $expr): int .[method] +-------------------------------- -Извличане на данни .[#toc-fetching-data] -======================================== +Връща сумата на стойностите в посочената колона или израз: -| `foreach ($table as $id => $row)` | итерация над всички редове с резултати -| `$row = $table->get($id)` | извличане на един низ с id $id от таблица -| `$row = $table->fetch()` | извличане на следващия низ от резултата -| `$array = $table->fetchPairs($key, $value)` | Изберете всички стойности като асоциативен масив -| `$array = $table->fetchPairs($value)` | получаване на всички записи в асоциативен масив -| `count($table)` | Получаване на броя на низовете в набора от резултати +```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): mixed .[method] +----------------------------------------------------------------------------- + +Позволява изпълнението на всяка функция за агрегиране. + +```php +// Изчислява средната цена на продуктите в дадена категория +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Комбинира етикетите на продуктите в един низ +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Ако трябва да обобщим резултати, които сами по себе си са резултат от обобщаване и групиране (например `SUM(value)` над групирани редове), като втори аргумент посочваме функцията за обобщаване, която да се приложи към тези междинни резултати: + +```php +// Изчислява общата цена на продуктите в наличност за всяка категория, след което сумира тези цени +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +В този пример първо изчисляваме общата цена на продуктите във всяка категория (`SUM(price * stock) AS category_total`) и групираме резултатите по `category_id`. След това използваме `aggregation('SUM(category_total)', 'SUM')`, за да съберем тези междинни суми. Вторият аргумент `'SUM'` указва функцията за агрегиране, която да се прилага към междинните резултати. Вмъкване, актуализиране и изтриване .[#toc-insert-update-delete] ================================================================ -Методът `insert()` приема масив от обекти Traversable (напр. [ArrayHash |utils:arrays#ArrayHash], който връща [форми |forms:]): +Nette Database Explorer опростява вмъкването, актуализирането и изтриването на данни. Всички споменати методи изхвърлят `Nette\Database\DriverException` в случай на грешка. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Вмъква нови записи в дадена таблица. + +**Вмъкване на единичен запис:** + +Новият запис се предава като асоциативен масив или итерабилен обект (като `ArrayHash`, използван във [формулярите |forms:]), където ключовете съответстват на имената на колоните в таблицата. + +Ако таблицата има дефиниран първичен ключ, методът връща обект `ActiveRow`, който се презарежда от базата данни, за да отрази всички промени, направени на ниво база данни (например тригери, стойности на колоните по подразбиране или изчисления за автоматично увеличаване). По този начин се осигурява последователност на данните и обектът винаги съдържа текущите данни от базата данни. Ако не е изрично дефиниран първичен ключ, методът връща входните данни като масив. ```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 е екземпляр на ActiveRow, който съдържа пълните данни на вмъкнатия ред, +// включително автоматично генерирания идентификатор и всички промени, направени от тригерите +echo $row->id; // Извежда идентификатора на нововместения потребител +echo $row->created_at; // Извежда времето на създаване, ако е зададено от тригер ``` -Ако за таблицата е дефиниран първичен ключ, се връща обект ActiveRow, съдържащ вмъкнатия ред. +**Вмъкване на няколко записа наведнъж:** -Вмъкване на няколко стойности: +Методът `insert()` ви позволява да вмъкнете няколко записа с една SQL заявка. В този случай той връща броя на вмъкнатите редове. ```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 ще бъде 2 +``` + +Можете също така да предадете като параметър обект `Selection` с избрани данни. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Като параметри могат да се предават файлове или обекти DateTime: +**Вмъкване на специални стойности:** + +Стойностите могат да включват файлове, обекти `DateTime` или SQL литерали: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // или $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // вмъква файл + 'name' => 'John', + 'created_at' => new DateTime, // конвертира във формат на база данни + 'avatar' => fopen('image.jpg', 'rb'), // вмъква съдържанието на двоичен файл + 'uuid' => $explorer::literal('UUID()'), // извиква функцията UUID() ]); ``` -Актуализиране (връща броя на засегнатите редове): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Обновява редове в таблица въз основа на зададен филтър. Връща броя на действително модифицираните редове. + +Колоните, които трябва да бъдат актуализирани, се предават като асоциативен масив или итерабилен обект (като `ArrayHash`, използван във [формуляри |forms:]), където ключовете съответстват на имената на колоните в таблицата: ```php -$count = $explorer->table('users') - ->where('id', 10) // трябва да се извика преди 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 ``` -Можем да използваме операторите `+=` и `-=`, за да актуализираме: +За промяна на числови стойности можете да използвате операторите `+=` и `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // увеличава стойността на колоната "точки" с 1 + 'coins-=' => 1, // намалява стойността на колоната "монети" с 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Delete (връща броя на изтритите редове): + +Selection::delete(): int .[method] +---------------------------------- + +Изтрива редове от таблица въз основа на зададен филтър. Връща броя на изтритите редове. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Когато извиквате `update()` или `delete()`, не забравяйте да използвате `where()`, за да посочите редовете, които трябва да бъдат актуализирани или изтрити. Ако не се използва `where()`, операцията ще бъде извършена върху цялата таблица! -Работа с взаимоотношения .[#toc-working-with-relationships] -=========================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- -Един към един ("има един") .[#toc-has-one-relation] ---------------------------------------------------- -Връзката "Един към един" е често срещан случай на употреба. Една книга има един автор. Книгата има един* преводач. Получаването на свързания низ се извършва главно чрез метода `ref()`. Тя приема два аргумента: името на целевата таблица и колоната на изходното присъединяване. Вижте пример: +Актуализира данните в ред от базата данни, представен от обекта `ActiveRow`. Той приема като параметър итерабилни данни, в които ключовете са имена на колони. За промяна на числови стойности можете да използвате операторите `+=` и `-=`: + +След извършване на актуализацията обектът `ActiveRow` автоматично се презарежда от базата данни, за да отрази всички промени, направени на ниво база данни (например тригери). Методът връща `true` само ако е настъпила реална промяна в данните. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // увеличава броя на прегледите +]); +echo $article->views; // Извеждане на текущия брой изгледи ``` -В горния пример извличаме свързания запис на автор от таблицата `author`, търсим първичния ключ на автора чрез колоната `book.author_id`. Методът Ref() връща екземпляр на ActiveRow или null, ако няма съответстващ запис. Върнатият ред е екземпляр на ActiveRow, така че можем да работим с него по същия начин, както със запис в книга. +Този метод актуализира само един конкретен ред в базата данни. За групови актуализации на множество редове използвайте метода [Selection::update(). |#Selection::update()] + + +ActiveRow::delete() .[method] +----------------------------- + +Изтрива ред от базата данни, който е представен от обекта `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Изтрива книгата с ID 1 +``` -// или директно -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Този метод изтрива само един конкретен ред в базата данни. За групово изтриване на множество редове използвайте метода [Selection::delete(). |#Selection::delete()] + + +Връзки между таблиците .[#toc-relationships-between-tables] +=========================================================== + +В релационните бази данни данните се разделят на няколко таблици и се свързват чрез външни ключове. Nette Database Explorer предлага революционен начин за работа с тези връзки - без да се пишат JOIN заявки или да се изисква каквато и да е конфигурация или генериране на същности. + +За демонстрация ще използваме **примерната база данни**[(налична в GitHub |https://github.com/nette-examples/books]). Базата данни включва следните таблици: + +- `author` - автори и преводачи (колони `id`, `name`, `web`, `born`) +- `book` - книги (колони `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - тагове (колони `id`, `name`) +- `book_tag` - таблица с връзки между книги и тагове (колони `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Структура на базата данни .<> + +В този пример за база данни с книги откриваме няколко вида връзки (опростени в сравнение с действителността): + +- Всяка книга има един** автор; един автор може да напише **множество** книги. +- **Нещо към много (0:N)** - Една книга може да има **преводач; един преводач може да превежда **многобройни** книги. +- **Нещо към едно (0:1)** - Една книга **може да има** продължение. +- **Много-към-много (M:N)** - Една книга **може да има няколко** тага, а един таг може да бъде присвоен на **няколко** книги. + +При тези връзки винаги има **родителска таблица** и **детска таблица**. Например във връзката между автори и книги таблицата `author` е родител, а таблицата `book` е дете - можете да си представите, че книгата винаги "принадлежи" на автор. Това е отразено и в структурата на базата данни: таблицата "дете" `book` съдържа външен ключ `author_id`, който препраща към таблицата "родител" `author`. + +Ако искаме да показваме книгите заедно с имената на техните автори, имаме две възможности. Или извличаме данните, като използваме една SQL заявка с JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Или извличаме данните на два етапа - първо книгите, а след това техните автори - и ги обединяваме в PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -В книгата има и един интерпретатор, така че е доста лесно да се открие името на интерпретатора. +Вторият подход е изненадващо **по-ефективен**. Данните се извличат само веднъж и могат да се използват по-добре в кеша. Точно по този начин работи Nette Database Explorer - той се справя с всичко под капака и ви предоставя чист 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 е запис от таблицата 'author' + echo 'translated by: ' . $book->translator?->name; +} ``` -Всичко това е добре, но е малко тромаво, не мислите ли? Database Explorer вече съдържа дефиниции на чужди ключове, така че защо да не ги използвате автоматично? Да го направим! -Ако извикаме свойство, което не съществува, ActiveRow се опитва да разреши името на извикващото свойство като връзката "има такъв". Извличането на това свойство е подобно на извикването на метода ref() само с един аргумент. Ще наричаме единичния аргумент **ключ**. Ключът ще бъде преобразуван в конкретна релация с външен ключ. Предаденият ключ се съпоставя с колоните на реда и ако съвпадне, чуждият ключ, дефиниран в съпоставената колона, се използва за извличане на данни от свързаната целева таблица. Вижте пример: +Достъп до родителската таблица .[#toc-accessing-the-parent-table] +----------------------------------------------------------------- + +Достъпът до родителската таблица е лесен. Това са връзки като *книга има автор* или *книга може да има преводач*. Достъпът до свързания запис може да се осъществи чрез обектното свойство `ActiveRow` - името на свойството съвпада с името на колоната на външния ключ без суфикса `id`: ```php -$book->author->name; -// същото -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // намира автора чрез колоната 'author_id'. +echo $book->translator?->name; // намира преводача чрез колоната "translator_id". ``` -Екземплярът на ActiveRow няма колона за автор. Всички колони в книгата се претърсват за съответствие с *ключ*. Съвпадение в този случай означава, че името на колоната трябва да съдържа ключ. Така че в примера по-горе колоната `author_id` съдържа символа "author" и следователно се съпоставя с ключа "author". Ако искате да получите преводача на книгата, можете да използвате например "translator" като ключ, тъй като ключът "translator" ще съответства на колоната `translator_id`. Можете да прочетете повече за логиката на съпоставяне на ключовете в глава [Съединяване на изрази |#joining-key]. +При достъп до свойството `$book->author` Explorer търси колона в таблицата `book`, която съдържа низ `author` (т.е. `author_id`). Въз основа на стойността в тази колона той извлича съответния запис от таблицата `author` и го връща като обект `ActiveRow`. По подобен начин `$book->translator` използва колоната `translator_id`. Тъй като колоната `translator_id` може да съдържа `null`, се използва операторът `?->`. + +Алтернативен подход се предлага от метода `ref()`, който приема два аргумента - името на целевата таблица и свързващата колона - и връща екземпляр на `ActiveRow` или `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; // връзка към автора +echo $book->ref('author', 'translator_id')->name; // връзка към преводача ``` -Ако искате да изтеглите няколко книги, използвайте същия подход. С помощта на Nette Database Explorer можете да намерите автори и преводачи за всички намерени книги едновременно. +Методът `ref()` е полезен, ако не може да се използва достъп, базиран на свойства, например когато таблицата съдържа колона със същото име като това на свойството (`author`). В други случаи се препоръчва използването на достъп, базиран на свойства, за по-добра четливост. + +Explorer автоматично оптимизира заявките към базата данни. При итерация през книги и достъп до свързаните с тях записи (автори, преводачи) Explorer не генерира заявка за всяка книга поотделно. Вместо това той изпълнява само **една SELECT заявка за всеки тип връзка**, което значително намалява натоварването на базата данни. Например: ```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; } ``` -Кодът ще изпълнява само тези 3 заявки: +Този код ще изпълни само три оптимизирани заявки към базата данни: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Логиката за идентифициране на свързващата колона се определя от изпълнението на [Conventions |api:Nette\Database\Conventions]. Препоръчваме ви да използвате [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], която анализира чуждите ключове и ви позволява да работите безпроблемно със съществуващите връзки между таблиците. -От един към много ("има много") .[#toc-has-many-relation] ---------------------------------------------------------- -Отношението "един към много" е просто обратното на отношението "един към един". Авторът *написал* *много* книги. Авторът е превел *много* книги. Както виждате, този тип релация е малко по-сложна, тъй като връзката е "поименна" ("написано", "преведено"). Инстанцията ActiveRow има метод `related()`, който връща масив от свързани записи. Записите също са екземпляри на ActiveRow. Вижте примера по-долу: +Достъп до подчинената таблица .[#toc-accessing-the-child-table] +--------------------------------------------------------------- + +Достъпът до подчинената таблица работи в обратна посока. Сега питаме *кои книги е написал този автор* или *кои книги е превел този преводач*. За този тип запитване използваме метода `related()`, който връща обект `Selection` със свързани записи. Ето един пример: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' написал:'; +$author = $explorer->table('author')->get(1); +// Извеждане на всички книги, написани от автора foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'и перевёл:'; +// Извеждане на всички книги, преведени от автора foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Методът `related()` приема пълно описание на връзката, предадено като два аргумента или като един аргумент, свързан с точка. Първият аргумент е целевата таблица, а вторият - целевата колона. +Методът `related()` приема описанието на връзката като единичен аргумент, използвайки точкова нотация, или като два отделни аргумента: + +```php +$author->related('book.translator_id'); // единичен аргумент +$author->related('book', 'translator_id'); // два аргумента +``` + +Explorer може автоматично да открие правилната свързваща колона въз основа на името на родителската таблица. В този случай той свързва чрез колоната `book.author_id`, тъй като името на изходната таблица е `author`: ```php -$author->related('book.translator_id'); -// то же самое -$author->related('book', 'translator_id'); +$author->related('book'); // използва book.author_id ``` -Можете да използвате евристиката на Nette Database Explorer, базирана на чужди ключове, и да посочите само аргумента **ключ**. Ключът ще бъде съпоставен с всички чужди ключове, сочещи към текущата таблица (таблица `author`). Ако има съвпадение, Nette Database Explorer ще използва този външен ключ, в противен случай ще хвърли [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] или [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Можете да прочетете повече за логиката на съпоставяне на ключовете в глава [Съединяване на изрази |#joining-key]. +Ако съществуват няколко възможни връзки, Explorer ще хвърли изключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Разбира се, можете да извикате свързаните методи за всички намерени автори и Nette Database Explorer ще извлече отново съответните книги наведнъж. +Разбира се, можем да използваме метода `related()` и при итерация през множество записи в цикъл, като Explorer автоматично ще оптимизира заявките и в този случай: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' написал:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -В горния пример ще бъдат извършени само две заявки: +Този код генерира само две ефективни SQL заявки: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- идентификаторы найденных авторов +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Ръчно създаване на Explorer .[#toc-creating-explorer-manually] -============================================================== +Връзка "много към много .[#toc-many-to-many-relationship] +--------------------------------------------------------- + +За връзка "много към много" (M:N) е необходима **свързваща таблица** (в нашия случай `book_tag`). Тази таблица съдържа две колони с външни ключове (`book_id`, `tag_id`). Всяка колона препраща към първичния ключ на една от свързаните таблици. За да извлечем свързани данни, първо извличаме записи от таблицата за връзка, като използваме `related('book_tag')`, а след това продължаваме към целевите данни: + +```php +$book = $explorer->table('book')->get(1); +// Извежда имената на таговете, зададени на книгата +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // извлича името на тага чрез таблицата за връзки +} + +$tag = $explorer->table('tag')->get(1); +// Противоположна посока: извежда заглавията на книгите с този таг +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // извлича заглавието на книгата +} +``` + +Explorer отново оптимизира SQL заявките в ефективна форма: -Връзката с база данни може да бъде създадена чрез конфигурацията на приложението. В такива случаи се създава услуга `Nette\Database\Explorer`, която може да бъде предадена като зависимост с помощта на DI-контейнера. +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Извършване на заявки чрез свързани таблици .[#toc-querying-through-related-tables] +---------------------------------------------------------------------------------- + +В методите `where()`, `select()`, `order()`, и `group()`, можете да използвате специални обозначения за достъп до колони от други таблици. Explorer автоматично създава необходимите JOIN-ове. + +Записът **Dot** (`parent_table.column`) се използва за връзки 1:N, гледани от гледна точка на родителската таблица: + +```php +$books = $explorer->table('book'); + +// Намира книги, чиито имена на автори започват с 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Подрежда книгите по името на автора в низходящ ред +$books->order('author.name DESC'); -Ако обаче Nette Database Explorer се използва като самостоятелен инструмент, трябва ръчно да се създаде екземпляр на обекта `Nette\Database\Explorer`. +// Извежда заглавието на книгата и името на автора +$books->select('book.title, author.name'); +``` + +**Записът с колонки** се използва за връзки 1:N от гледна точка на родителската таблица: ```php -// $storage имплементира Nette\Caching\Storage: -$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); +$authors = $explorer->table('author'); + +// Намира автори, които са написали книга с 'PHP' в заглавието +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Отчита броя на книгите за всеки автор +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +В горния пример с двоеточие (`:book.title`) колоната на външния ключ не е изрично посочена. Explorer автоматично открива правилната колона въз основа на името на родителската таблица. В този случай той се присъединява чрез колоната `book.author_id`, тъй като името на изходната таблица е `author`. Ако съществуват няколко възможни връзки, Explorer хвърля изключението [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Свързващата колона може да бъде изрично посочена в скоби: + +```php +// Намира автори, превели книга с 'PHP' в заглавието +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Нотациите могат да бъдат верижно свързани, за да се получи достъп до данни в няколко таблици: + +```php +// Намира автори на книги с етикет 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Разширяване на условията за JOIN .[#toc-extending-conditions-for-join] +---------------------------------------------------------------------- + +Методът `joinWhere()` добавя допълнителни условия за обединяване на таблици в SQL след ключовата дума `ON`. + +Например, да кажем, че искаме да намерим книги, преведени от определен преводач: + +```php +// Намира книги, преведени от преводач с име 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +В условието `joinWhere()` можете да използвате същите конструкции като в метода `where()` - оператори, заместители, масиви от стойности или SQL изрази. + +За по-сложни заявки с множество JOIN-и могат да се дефинират псевдоними на таблици: + +```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` +// И (`book_author`.`born` < 1950) +``` + +Обърнете внимание, че докато методът `where()` добавя условия към клаузата `WHERE`, методът `joinWhere()` разширява условията в клаузата `ON` по време на обединяване на таблици. + + +Ръчно създаване на Explorer .[#toc-manually-creating-explorer] +============================================================== + +Ако не използвате контейнера Nette DI, можете да създадете инстанция на `Nette\Database\Explorer` ръчно: + +```php +use Nette\Database; + +// $storage имплементира Nette\Caching\Storage, например: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// връзка с база данни +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// управлява отразяването на структурата на базата данни +$structure = new Database\Structure($connection, $storage); +// дефинира правила за съпоставяне на имена на таблици, колони и външни ключове +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/bg/security.texy b/database/bg/security.texy new file mode 100644 index 0000000000..5d71092fd4 --- /dev/null +++ b/database/bg/security.texy @@ -0,0 +1,145 @@ +Риск за сигурността +******************* + +.[perex] +Базите данни често съдържат чувствителни данни и позволяват опасни операции. Базата данни Nette предоставя редица функции за сигурност. От решаващо значение е обаче да се разбере разликата между безопасни и опасни API. + + +SQL инжектиране .[#toc-sql-injection] +===================================== + +SQL инжектирането е най-сериозният риск за сигурността при работа с бази данни. То възниква, когато непроверен потребителски вход стане част от SQL заявка. Атакуващият може да инжектира свои собствени SQL команди, като по този начин получава или променя данни в базата данни. + +```php +// ❌ БЕЗОПАСЕН КОД - уязвим към SQL инжекция +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Нападателят може да въведе нещо подобно на: ' ИЛИ '1'='1 +// Получената заявка ще бъде: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Това връща всички потребители! +``` + +Същото се отнася и за Database Explorer: + +```php +// ❌ КОД ЗА БЕЗОПАСНОСТ +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Безопасни параметризирани заявки .[#toc-safe-parameterized-queries] +=================================================================== + +Безопасният начин за вмъкване на стойности в SQL заявките е чрез параметризирани заявки. Базата данни Nette предоставя няколко начина за използването им. + + +Заместващи въпросителни знаци .[#toc-placeholder-question-marks] +---------------------------------------------------------------- + +Най-простият метод е да се използват заместващи въпросителни знаци: + +```php +// ✅ Безопасни параметризирани заявки +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Безопасно условие в Explorer +$table->where('name = ?', $_GET['name']); +``` + +Същото важи и за всички други методи в Database Explorer, които позволяват вмъкване на изрази с въпросителни знаци и параметри. + +.[warning] +Стойностите трябва да са от скаларен тип (`string`, `int`, `float`, `bool`) или `null`. Ако например `$_GET['name']` е масив, Nette Database ще включи всички негови елементи в SQL заявката, което може да е нежелателно. + + +Масиви от стойности .[#toc-value-arrays] +---------------------------------------- + +За клаузите `INSERT`, `UPDATE` или `WHERE` можем да използваме масиви от стойности: + +```php +// ✅ Безопасен INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Безопасен UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database автоматично ескапира всички стойности, предадени чрез параметризирани заявки. Трябва обаче да осигурим правилния тип данни на параметрите. + + +Ключовете на масива не са сигурен API .[#toc-array-keys-are-not-a-safe-api] +=========================================================================== + +Докато стойностите в масивите са безопасни, същото не може да се каже за ключовете: + +```php +// ❌ БЕЗОПАСЕН КОД - ключовете могат да съдържат SQL инжекция +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +За командите `INSERT` и `UPDATE` това е критичен недостатък в сигурността - атакуващият може да вмъкне или промени всяка колона в базата данни. Например, той може да зададе `is_admin = 1` или да вмъкне произволни данни в чувствителни колони. + +В условията на `WHERE` това е още по-опасно, защото позволява **SQL enumeration** - техника за постепенно извличане на информация за базата данни. Нападателят може да се опита да проучи заплатите на служителите, като инжектира в `$_GET` по този начин: + +```php +$_GET = ['salary >', 100000]; // започва да определя диапазони на заплатите. +``` + +Основният проблем обаче е, че `WHERE` условията поддържат SQL изрази в ключовете: + +```php +// Законосъобразно използване на оператори в ключовете +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ БЕЗОПАСНО: атакуващият може да инжектира свой собствен SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // позволява на нападателя да получи администраторски заплати +``` + +Това отново е **SQL инжекция**. + + +Създаване на бял списък на колоните .[#toc-whitelisting-columns] +---------------------------------------------------------------- + +Ако искате да разрешите на потребителите да избират колони, винаги използвайте бял списък: + +```php +// ✅ Безопасна обработка - само разрешени колони +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Динамични идентификатори .[#toc-dynamic-identifiers] +==================================================== + +За динамични имена на таблици и колони използвайте заместителя `?name`: + +```php +// ✅ Безопасно използване на надеждни идентификатори +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - никога не използвайте потребителски вход +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Символът `?name` трябва да се използва само за доверени стойности, дефинирани в кода на приложението. За стойности, предоставени от потребителя, отново използвайте бял списък. diff --git a/database/cs/@left-menu.texy b/database/cs/@left-menu.texy index df663673b2..8353639528 100644 --- a/database/cs/@left-menu.texy +++ b/database/cs/@left-menu.texy @@ -4,4 +4,5 @@ Databáze - [Explorer] - [Reflexe |reflection] - [Konfigurace |configuration] +- [Bezpečnostní rizika |security] - [Upgrade |upgrading] diff --git a/database/cs/explorer.texy b/database/cs/explorer.texy index d1f397fd5a..6dd5669964 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], +]); +``` -// 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() ---------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Příklad použití bez parametrů: +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] +--------------------------------------------------------- -Příklady použití: +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 -$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) +// 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 +foreach ($table as $row) { + echo $row->formatted_date; // přístup k aliasu +} +``` -limit() -------- -Příklady použití: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +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 +========= -Příklady použití: +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 +-------------------------------------- + +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', ...] ``` -Teď se podívejme na složitější příklad na skládání tabulek. +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: -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'. +```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', ...] +``` + + +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` = ? +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('name = ' . $name); // ŠPATNĚ! sql injection! -->where('name = ?', $name); // SPRÁVNĚ -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // ŠPATNĚ! hodnoty dosazujeme přes parametr -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // SPRÁVNĚ +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: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` -| `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 + +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] +------------------------------------------------- +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'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // zvýšíme počet zobrazení +]); +echo $article->views; // Vypíše aktuální počet zobrazení ``` -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()]. -// nebo přímo -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +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`) + +[* 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 ``` -Kniha má také jednoho překladatele, jeho jméno získáme snadno. +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 +``` + +Druhý přístup je ve skutečnosti efektivnější, i když to může být překvapivé. Data jsou načtena pouze jednou a mohou být lépe využita v cache. Právě tímto způsobem pracuje Nette Database Explorer - vše řeší pod povrchem a vám nabízí elegantní 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 `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` a vrátí jej jako `ActiveRow`. Podobně funguje i `$book->translator`, který využije sloupec `translator_id`. Protože 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á vrátí `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 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`: -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. +```php +$author->related('book'); // použije book.author_id +``` + +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,131 @@ 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. Pro získání souvisejících dat 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; // vypíše název knihy +} +``` + +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]. + +Spojovací sloupec lze 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'); +``` + + +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/security.texy b/database/cs/security.texy new file mode 100644 index 0000000000..07eede6987 --- /dev/null +++ b/database/cs/security.texy @@ -0,0 +1,145 @@ +Bezpečnostní rizika +******************* + +.[perex] +Databáze často obsahuje citlivá data a umožňuje provádět nebezpečné operace. Nette Database nabízí řadu bezpečnostních prvků. Klíčové je ale pochopení rozdílu mezi bezpečným a nebezpečným API. + + +SQL Injection +============= + +SQL injection je nejzávažnější bezpečnostní riziko při práci s databází. Vzniká, když se neošetřený vstup od uživatele stane součástí SQL dotazu. Útočník může vložit vlastní SQL příkazy a tím získat nebo modifikovat data v databázi. + +```php +// ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Útočník může zadat například hodnotu: ' OR '1'='1 +// Výsledný dotaz pak bude: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Což vrátí všechny uživatele! +``` + +Totéž se týká i Database Explorer: + +```php +// ❌ NEBEZPEČNÝ KÓD +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Bezpečné parametrizované dotazy +=============================== + +Bezpečným způsobem vkládání hodnot do SQL dotazů jsou parametrizované dotazy. Nette Database nabízí několik způsobů jejich použití. + + +Zástupné otazníky +----------------- + +Nejjednodušší způsob je použití zástupných otazníků: + +```php +// ✅ Bezpečné parametrizované dotazy +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Bezpečná podmínka v Exploreru +$table->where('name = ?', $_GET['name']); +``` + +Totéž platí pro všechny další metody v Database Explorer, které umožňují vkládat výrazy se zástupnými otazníky a parametry. + +.[warning] +Hodnoty musí být skalárního typu (string, int, float, bool) nebo null. Pokud by například `$_GET['name']` bylo pole, Nette Database by vložil do SQL všechny jeho prvky, což může být nežádoucí. + + +Pole hodnot +----------- + +Pro příkazy INSERT, UPDATE nebo klauzule WHERE můžeme použít pole hodnot: + +```php +// ✅ Bezpečný INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Bezpečný UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database automaticky escapuje všechny hodnoty předané přes parametrizované dotazy. Musíme však zajistit správný datový typ parametrů. + + +Klíče polí nejsou bezpečné API +============================== + +Zatímco hodnoty v polích jsou bezpečné, o klíčích to neplatí: + +```php +// ❌ NEBEZPEČNÝ KÓD - klíče mohou obsahovat SQL injection +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +U příkazů INSERT a UPDATE je to zásadní bezpečnostní chyba - útočník může do databáze vložit nebo změnit jakýkoliv sloupec. Mohl by si například nastavit `is_admin = 1` nebo vložit libovolná data do citlivých sloupců. + +Ve WHERE podmínkách je to ještě nebezpečnější, protože umožňuje SQL enumeration - techniku postupného zjišťování informací o databázi. Útočník může třeba zkoumat plat zaměstnanců tím, že do `$_GET` podstrčí: + +```php +$_GET = ['salary >', 100000]; // začne zjišťovat platové rozsahy +``` + +Ale hlavní problém je, že WHERE podmínky podporují v klíčích SQL výrazy: + +```php +// Legitimní použití operátorů v klíčích +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ NEBEZPEČNÉ: útočník může vložit vlastní SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // umožní útočníkovi získat platy adminů +``` + +Toto je opět **SQL injection**. + + +Whitelist sloupců +----------------- + +Pokud chcete uživateli umožnit volbu sloupců, vždy použijte whitelist: + +```php +// ✅ Bezpečné zpracování - pouze povolené sloupce +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dynamické identifikátory +======================== + +Pro dynamické názvy tabulek a sloupců použijte zástupný symbol `?name`: + +```php +// ✅ Bezpečné použití důvěryhodných identifikátorů +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ NEBEZPEČNÉ - nikdy nepoužívejte vstup od uživatele +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Symbol `?name` používejte pouze pro důvěryhodné hodnoty definované v kódu aplikace. Pro hodnoty od uživatele použijte opět whitelist. diff --git a/database/de/@left-menu.texy b/database/de/@left-menu.texy index 97836f6264..a9519260e6 100644 --- a/database/de/@left-menu.texy +++ b/database/de/@left-menu.texy @@ -4,3 +4,4 @@ Datenbank - [Entdecker |Explorer] - [Überlegungen |Reflection] - [Konfiguration |Configuration] +- [Sicherheitsrisiken |security] diff --git a/database/de/explorer.texy b/database/de/explorer.texy index 10784b7089..00d8901071 100644 --- a/database/de/explorer.texy +++ b/database/de/explorer.texy @@ -3,548 +3,927 @@ Datenbank-Explorer
-Der Nette Database Explorer vereinfacht das Abrufen von Daten aus der Datenbank erheblich, ohne dass SQL-Abfragen geschrieben werden müssen. +Der Nette Database Explorer ist eine leistungsstarke Schicht, die den Abruf von Daten aus der Datenbank erheblich vereinfacht, ohne dass SQL-Abfragen geschrieben werden müssen. -- verwendet effiziente Abfragen -- keine Daten werden unnötig übertragen -- verfügt über eine elegante Syntax +- Die Arbeit mit Daten ist natürlich und einfach zu verstehen +- Generiert optimierte SQL-Abfragen, die nur die notwendigen Daten abrufen +- Bietet einfachen Zugriff auf verwandte Daten, ohne dass JOIN-Abfragen geschrieben werden müssen +- Funktioniert sofort, ohne dass eine Konfiguration oder Entitätserstellung erforderlich ist
-Um den Database Explorer zu verwenden, beginnen Sie mit einer Tabelle - rufen Sie `table()` auf einem [api:Nette\Database\Explorer] Objekt auf. Der einfachste Weg, um eine Kontextobjektinstanz zu erhalten, ist [hier beschrieben |core#Connection and Configuration], oder, für den Fall, dass Nette Database Explorer als eigenständiges Werkzeug verwendet wird, kann es [manuell erstellt |#Creating Explorer Manually] werden. +Der Nette Database Explorer ist eine Erweiterung der [Nette Database Core |core] Schicht, die einen bequemen objektorientierten Ansatz für die Datenbankverwaltung bietet. + +Die Arbeit mit dem Explorer beginnt mit dem Aufruf der Methode `table()` für das Objekt [api:Nette\Database\Explorer] (wie man es erhält, wird [hier beschrieben |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // db table name is 'book' +$books = $explorer->table('book'); // 'book' ist der Name der Tabelle ``` -Der Aufruf gibt eine Instanz des [Selection-Objekts |api:Nette\Database\Table\Selection] zurück, das durchlaufen werden kann, um alle Bücher abzurufen. Jedes Element (eine Zeile) wird durch eine Instanz von [ActiveRow |api:Nette\Database\Table\ActiveRow] mit Daten dargestellt, die seinen Eigenschaften zugeordnet sind: +Die Methode gibt ein [Selection-Objekt |api:Nette\Database\Table\Selection] zurück, das eine SQL-Abfrage darstellt. Zusätzliche Methoden können mit diesem Objekt verkettet werden, um die Ergebnisse zu filtern und zu sortieren. Die Abfrage wird nur zusammengestellt und ausgeführt, wenn die Daten angefordert werden, z. B. durch Iteration mit `foreach`. Jede Zeile wird durch ein [ActiveRow-Objekt |api:Nette\Database\Table\ActiveRow] dargestellt: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // gibt die Spalte "Titel" aus + echo $book->author_id; // gibt die Spalte "author_id" aus } ``` -Das Abrufen einer bestimmten Zeile erfolgt über die Methode `get()`, die direkt eine ActiveRow-Instanz zurückgibt. +Der Explorer vereinfacht die Arbeit mit [Tabellenbeziehungen |#Vazby mezi tabulkami] erheblich. Das folgende Beispiel zeigt, wie einfach wir Daten aus verwandten Tabellen (Bücher und ihre Autoren) ausgeben können. Beachten Sie, dass keine JOIN-Abfragen geschrieben werden müssen; Nette generiert sie für uns: ```php -$book = $explorer->table('book')->get(2); // gibt das Buch mit der Nummer 2 zurück -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // erstellt einen JOIN zur Tabelle 'author'. +} ``` -Schauen wir uns einen häufigen Anwendungsfall an. Sie müssen Bücher und ihre Autoren abrufen. Dies ist eine übliche 1:N-Beziehung. Die häufig verwendete Lösung besteht darin, die Daten mit einer SQL-Abfrage mit Tabellen-Joins abzurufen. Die zweite Möglichkeit besteht darin, die Daten separat abzurufen, eine Abfrage zum Abrufen der Bücher auszuführen und dann mit einer anderen Abfrage (z. B. in Ihrem foreach-Zyklus) einen Autor für jedes Buch zu ermitteln. Dies könnte leicht so optimiert werden, dass nur zwei Abfragen ausgeführt werden, eine für die Bücher und eine weitere für die benötigten Autoren - und genau so macht es der Nette Database Explorer. +Nette Database Explorer optimiert die Abfragen für maximale Effizienz. Das obige Beispiel führt nur zwei SELECT-Abfragen aus, unabhängig davon, ob wir 10 oder 10.000 Bücher verarbeiten. -In den folgenden Beispielen werden wir mit dem Datenbankschema in der Abbildung arbeiten. Es gibt OneHasMany (1:N)-Verknüpfungen (Autor des Buches `author_id` und möglicher Übersetzer `translator_id`, der `null` sein kann) und ManyHasMany (M:N)-Verknüpfungen zwischen Buch und seinen Tags. +Darüber hinaus verfolgt der Explorer, welche Spalten im Code verwendet werden, und holt nur diese aus der Datenbank, um weitere Leistung zu sparen. Dieses Verhalten ist vollautomatisch und anpassungsfähig. Wenn Sie den Code später ändern, um zusätzliche Spalten zu verwenden, passt der Explorer die Abfragen automatisch an. Sie brauchen nichts zu konfigurieren oder darüber nachzudenken, welche Spalten benötigt werden - überlassen Sie das Nette. -[Ein Beispiel, einschließlich eines Schemas, ist auf GitHub zu finden |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** In den Beispielen verwendete Datenbankstruktur .<> +Filtern und Sortieren .[#toc-filtering-and-sorting] +=================================================== -Der folgende Code listet den Namen des Autors für jedes Buch und alle seine Tags auf. Wir werden [gleich besprechen |#Working with relationships], wie dies intern funktioniert. +Die Klasse `Selection` bietet Methoden zum Filtern und Sortieren von Daten. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Fügt eine WHERE-Bedingung hinzu. Mehrere Bedingungen werden mit AND kombiniert. +| `whereOr(array $conditions)` | Fügt eine Gruppe von WHERE-Bedingungen hinzu, die mit OR kombiniert werden | +| `wherePrimary($value)` | Fügt eine WHERE-Bedingung basierend auf dem Primärschlüssel hinzu | +| `order($columns, ...$params)` | Legt die Sortierung mit ORDER BY fest | +| `select($columns, ...$params)` | Legt fest, welche Spalten abgerufen werden sollen | +| `limit($limit, $offset = null)` | Begrenzt die Anzahl der Zeilen (LIMIT) und setzt optional OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Legt die Paginierung fest | +| `group($columns, ...$params)` | Gruppiert Zeilen (GROUP BY) | +| `having($condition, ...$params)`| Fügt eine HAVING-Bedingung zum Filtern gruppierter Zeilen hinzu | -foreach ($books as $book) { - echo 'Titel: ' . $book->title; - echo 'geschrieben von: ' . $book->author->name; // $book->autor ist Zeile aus Tabelle 'autor' +Methoden können verkettet werden (sogenannte [fließende Schnittstelle |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 ist Zeile aus Tabelle 'tag' - } -} -``` +Diese Methoden erlauben auch die Verwendung spezieller Notationen für den Zugriff auf [Daten aus Bezugstabellen |#Dotazování přes související tabulky]. -Sie werden erfreut sein, wie effizient die Datenbankschicht arbeitet. Das obige Beispiel stellt eine konstante Anzahl von Anfragen, die wie folgt aussehen: -```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)) -``` +Escaping und Bezeichner .[#toc-escaping-and-identifiers] +-------------------------------------------------------- -Wenn Sie den [Cache |caching:] verwenden (standardmäßig eingeschaltet), werden keine Spalten unnötig abgefragt. Nach der ersten Abfrage speichert der Cache die verwendeten Spaltennamen und Nette Database Explorer führt nur Abfragen mit den benötigten Spalten aus: +Die Methoden entschlüsseln automatisch Parameter und Bezeichner (Tabellen- und Spaltennamen), um SQL-Injection zu verhindern. Um einen ordnungsgemäßen Betrieb zu gewährleisten, müssen einige Regeln beachtet werden: -```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)) +- Schreiben Sie Schlüsselwörter, Funktionsnamen, Prozeduren usw. in **Großbuchstaben**. +- Schreiben Sie Spalten- und Tabellennamen in **Kleinbuchstaben**. +- Übergeben Sie Zeichenketten immer mit **Parametern**. + +```php +where('name = ' . $name); // **DISASTER**: anfällig für SQL-Injection +where('name LIKE "%search%"'); // **FALSCH**: erschwert die automatische Quotierung +where('name LIKE ?', '%search%'); // **CORRECT**: Wert wird als Parameter übergeben + +where('name like ?', $name); // **WRONG**: erzeugt: `Name` `wie` ? +where('name LIKE ?', $name); // **KORREKT**: erzeugt: `Name` LIKE ? +where('LOWER(name) = ?', $value);// **RICHTIG**: LOWER(`name`) = ? ``` -Auswahlen .[#toc-selections] -============================ +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Siehe Möglichkeiten zum Filtern und Einschränken von Zeilen [api:Nette\Database\Table\Selection]: +Filtert Ergebnisse mithilfe von WHERE-Bedingungen. Seine Stärke liegt in der intelligenten Handhabung verschiedener Wertetypen und der automatischen Auswahl von SQL-Operatoren. -.[language-php] -| `$table->where($where[, $param[, ...]])` | WHERE mit AND als Verknüpfung setzen, wenn zwei oder mehr Bedingungen angegeben werden -| `$table->whereOr($where)` | WHERE setzen und OR als Verknüpfung verwenden, wenn zwei oder mehr Bedingungen angegeben werden -| `$table->order($columns)` | ORDER BY setzen, kann Ausdruck sein `('column DESC, id DESC')` -| `$table->select($columns)` | Set abgerufenen Spalten, kann Ausdruck sein `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | LIMIT und OFFSET setzen -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Aktiviert Umbruch -| `$table->group($columns)` | GROUP BY einstellen -| `$table->having($having)` | HAVING einstellen +Grundlegende Verwendung: -Wir können eine sogenannte [fließende Schnittstelle |nette:introduction-to-object-oriented-programming#fluent-interfaces] verwenden, zum Beispiel `$table->where(...)->order(...)->limit(...)`. Mehrere `where` oder `whereOr` Bedingungen werden mit dem Operator `AND` verknüpft. +```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' +``` +Dank der automatischen Erkennung geeigneter Operatoren müssen Sie sich nicht um Spezialfälle kümmern - Nette erledigt das für Sie: -wo() .[#toc-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) +// Der Platzhalter ? kann ohne Operator verwendet werden: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer kann automatisch die benötigten Operatoren für übergebene Werte hinzufügen: +Die Methode geht auch mit negativen Bedingungen und leeren Feldern korrekt um: -.[language-php] -| `$table->where('field', $value)` | feld = $wert -| `$table->where('field', null)` | feld IST NULL -| `$table->where('field > ?', $val)` | feld > $wert -| `$table->where('field', [1, 2])` | feld IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OR name = 'Jon Snow' -| `$table->where('field', $explorer->table($tableName))` | feld IN (SELECT $primär FROM $tabellenname) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | feld IN (SELECT col FROM $tabellenname) +```php +$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- findet nichts +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- findet alles +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- findet alles +// $table->where('NOT id ?', $ids); // WARNUNG: Diese Syntax wird nicht unterstützt +``` -Sie können Platzhalter auch ohne Spaltenoperator angeben. Diese Aufrufe sind die gleichen. +Sie können auch das Ergebnis einer anderen Tabellenabfrage als Parameter übergeben und so eine Unterabfrage erstellen: ```php -$table->where('id = ? OR id = ?', 1, 2); -$table->where('id ? OR id ?', 1, 2); +// WHERE `id` IN (SELECT `id` FROM `Tabellenname`) +$table->where('id', $explorer->table($tableName)); + +// WHERE `id` IN (SELECT `col` FROM `Tabellenname`) +$table->where('id', $explorer->table($tableName)->select('col')); ``` -Diese Funktion ermöglicht es, den richtigen Operator basierend auf dem Wert zu generieren: +Bedingungen können auch als Array übergeben werden, wobei die Elemente mit AND kombiniert werden: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`Preis_endgültig` < `Preis_originell`) AND (`Bestand_Zahl` > `min_Bestand`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Die Auswahl behandelt auch negative Bedingungen korrekt und funktioniert auch bei leeren Feldern: +In dem Array können Schlüssel-Wert-Paare verwendet werden, und Nette wählt wieder automatisch die richtigen Operatoren aus: ```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` = 'aktiv') AND (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` + +Wir können auch SQL-Ausdrücke mit Platzhaltern und mehreren Parametern mischen. Dies ist nützlich für komplexe Bedingungen mit genau definierten Operatoren: -// dies führt zu einer Ausnahme, da diese Syntax nicht unterstützt wird -$table->where('NOT id ?', $ids); +```php +// WHERE (`Alter` > 18) AND (RUND(`Punktzahl`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // zwei Parameter werden als Array übergeben +]); ``` +Mehrere Aufrufe von `where()` kombinieren die Bedingungen automatisch mit AND. -whereOr() .[#toc-whereor] -------------------------- -Beispiel für die Verwendung ohne Parameter: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Ähnlich wie `where()`, kombiniert jedoch Bedingungen mit OR: ```php -// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`)) +// WHERE (`status` = 'aktiv') OR (`gelöscht` = 1) $table->whereOr([ - 'user_id IS NULL', - 'SUM(field1) > SUM(field2)', + 'status' => 'active', + 'deleted' => true, ]); ``` -Wir verwenden die Parameter. Wenn Sie keinen Operator angeben, fügt Nette Database Explorer automatisch den entsprechenden Operator hinzu: +Es können auch komplexere Ausdrücke verwendet werden: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`Preis` > 1000) OR (`Preis_mit_Steuer` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -Der Schlüssel kann einen Ausdruck enthalten, der Fragezeichen als Platzhalter enthält, und dann Parameter im Wert übergeben: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Fügt eine Bedingung für den Primärschlüssel der Tabelle hinzu: ```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]); +``` + +Wenn die Tabelle einen zusammengesetzten Primärschlüssel hat (z. B. `foo_id`, `bar_id`), wird dieser als Array übergeben: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Beispiele für die Verwendung: +Legt die Reihenfolge fest, in der die Zeilen zurückgegeben werden. Sie können nach einer oder mehreren Spalten, in aufsteigender oder absteigender Reihenfolge oder nach einem benutzerdefinierten Ausdruck sortieren: ```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 `erstellt` +$table->order('created DESC'); // ORDER BY `erstellt` DESC +$table->order('priority DESC, created'); // ORDER BY `priorität` DESC, `erstellt` +$table->order('status = ? DESC', 'active'); // ORDER BY `Status` = 'aktiv' DESC ``` -select() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Beispiele für die Verwendung: +Gibt die Spalten an, die aus der Datenbank zurückgegeben werden sollen. Standardmäßig gibt der Nette Database Explorer nur die Spalten zurück, die tatsächlich im Code verwendet werden. Verwenden Sie die Methode `select()`, wenn Sie bestimmte Ausdrücke abrufen müssen: ```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) +// SELECT *, DATE_FORMAT(`erstellt_am`, "%d.%m.%Y") AS `formatiertes_datum` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Die mit `AS` definierten Aliase sind dann als Eigenschaften des `ActiveRow` Objekts zugänglich: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // Zugriff auf den Alias +} +``` -limit() .[#toc-limit] ---------------------- -Beispiele für die Verwendung: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Begrenzt die Anzahl der zurückgegebenen Zeilen (LIMIT) und legt optional einen Offset fest: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (liefert die ersten 10 Zeilen) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Für die Paginierung ist es sinnvoller, die Methode `page()` zu verwenden. + -page() .[#toc-page] -------------------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Eine alternative Möglichkeit, die Grenze und den Versatz festzulegen: +Vereinfacht die Seitennummerierung der Ergebnisse. Sie akzeptiert die Seitenzahl (beginnend mit 1) und die Anzahl der Elemente pro Seite. Optional können Sie einen Verweis auf eine Variable übergeben, in der die Gesamtzahl der Seiten gespeichert wird: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Abrufen der letzten Seitenzahl, die an die Variable `$lastPage` übergeben wird: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Gruppiert Zeilen nach den angegebenen Spalten (GROUP BY). Sie wird normalerweise in Kombination mit Aggregatfunktionen verwendet: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Zählt die Anzahl der Produkte in jeder Kategorie +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Beispiele für die Verwendung: +Legt eine Bedingung für das Filtern von gruppierten Zeilen fest (HAVING). Sie kann in Kombination mit der Methode `group()` und Aggregatfunktionen verwendet werden: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Findet Kategorien mit mehr als 100 Produkten +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Daten lesen +=========== + +Für das Lesen von Daten aus der Datenbank stehen mehrere nützliche Methoden zur Verfügung: + +.[language-php] +| `foreach ($table as $key => $row)` | Iteriert durch alle Zeilen, `$key` ist der Primärschlüsselwert, `$row` ist ein ActiveRow-Objekt | +| `$row = $table->get($key)` | Gibt eine einzelne Zeile nach Primärschlüssel zurück | +| `$row = $table->fetch()` | Gibt die aktuelle Zeile zurück und schiebt den Zeiger auf die nächste Zeile vor | +| `$array = $table->fetchPairs()` | Erstellt ein assoziatives Array aus den Ergebnissen | +| `$array = $table->fetchAll()` | Gibt alle Zeilen als Array zurück | +| `count($table)` | Gibt die Anzahl der Zeilen im Selection-Objekt zurück | + +Das [ActiveRow-Objekt |api:Nette\Database\Table\ActiveRow] ist schreibgeschützt. Das bedeutet, dass Sie die Werte seiner Eigenschaften nicht ändern können. Diese Einschränkung gewährleistet die Datenkonsistenz und verhindert unerwartete Nebeneffekte. Die Daten werden aus der Datenbank geholt, und alle Änderungen sollten ausdrücklich und kontrolliert vorgenommen werden. + + +`foreach` - Iterieren durch alle Zeilen +--------------------------------------- -Beispiele für die Verwendung: +Der einfachste Weg, eine Abfrage auszuführen und Zeilen abzurufen, ist die Iteration mit der `foreach` -Schleife. Dabei wird die SQL-Abfrage automatisch ausgeführt. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = Primärschlüssel, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtern nach einem anderen Tabellenwert .[#toc-joining-key] ------------------------------------------------------------ +get($key): ?ActiveRow .[method] +------------------------------- + +Führt eine SQL-Abfrage aus und gibt eine Zeile anhand ihres Primärschlüssels oder `null` zurück, wenn sie nicht vorhanden ist. + +```php +$book = $explorer->table('book')->get(123); // gibt ActiveRow mit ID 123 oder null zurück +if ($book) { + echo $book->title; +} +``` -Häufig müssen Sie Ergebnisse nach einer Bedingung filtern, die eine andere Datenbanktabelle betrifft. Diese Arten von Bedingungen erfordern einen Tabellen-Join. Sie brauchen sie jedoch nicht mehr zu schreiben. -Nehmen wir an, Sie wollen alle Bücher finden, deren Autor 'Jon' heißt. Alles, was Sie schreiben müssen, ist der Verknüpfungsschlüssel der Beziehung und der Spaltenname in der verknüpften Tabelle. Der Verknüpfungsschlüssel wird von der Spalte abgeleitet, die sich auf die Tabelle bezieht, die Sie verknüpfen wollen. In unserem Beispiel (siehe das DB-Schema) ist dies die Spalte `author_id`, und es genügt, nur den ersten Teil davon zu verwenden - `author` (das Suffix `_id` kann weggelassen werden). `name` ist eine Spalte in der Tabelle `author`, die wir verwenden möchten. Eine Bedingung für den Buchübersetzer (der mit der Spalte `translator_id` verbunden ist) kann ebenso einfach erstellt werden. +fetch(): ?ActiveRow .[method] +----------------------------- + +Gibt eine Zeile zurück und rückt den internen Zeiger auf die nächste Zeile vor. Wenn es keine weiteren Zeilen gibt, wird `null` zurückgegeben. ```php $books = $explorer->table('book'); -$books->where('author.name LIKE ?', '%Jon%'); -$books->where('translator.name', 'David Grudl'); +while ($book = $books->fetch()) { + $this->processBook($book); +} ``` -Die Logik der Verbindungsschlüssel wird durch die Implementierung von [Conventions |api:Nette\Database\Conventions] bestimmt. Wir empfehlen die Verwendung von [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], die Ihre Fremdschlüssel analysiert und es Ihnen ermöglicht, einfach mit diesen Beziehungen zu arbeiten. -Die Beziehung zwischen dem Buch und seinem Autor ist 1:N. Die umgekehrte Beziehung ist ebenfalls möglich. Wir nennen sie **backjoin**. Schauen Sie sich ein anderes Beispiel an. Wir möchten alle Autoren abrufen, die mehr als 3 Bücher geschrieben haben. Um die Verknüpfung umzukehren, verwenden wir die Anweisung `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY`, und auch die Bedingung muss in Form der Anweisung `HAVING` geschrieben werden. +fetchPairs(): array .[method] +----------------------------- + +Gibt die Ergebnisse in Form eines assoziativen Arrays zurück. Das erste Argument gibt den Spaltennamen an, der als Schlüssel im Array verwendet werden soll, und das zweite Argument gibt den Spaltennamen an, der als Wert verwendet werden soll: ```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', ...] +``` + +Wenn nur die Schlüsselspalte angegeben wird, ist der Wert die gesamte Zeile, d. h. das Objekt `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Sie haben vielleicht bemerkt, dass sich der Verknüpfungsausdruck auf das Buch bezieht, aber es ist nicht klar, ob wir über `author_id` oder `translator_id` verknüpfen. Im obigen Beispiel verknüpft Selection über die Spalte `author_id`, weil eine Übereinstimmung mit der Quelltabelle gefunden wurde - der Tabelle `author`. Wenn es keine solche Übereinstimmung gäbe und es mehrere Möglichkeiten gäbe, würde Nette eine [AmbiguousReferenceKeyException aus lösen|api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Wenn `null` als Schlüssel angegeben ist, wird das Array numerisch indiziert, beginnend bei Null: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` -Um eine Verknüpfung über die Spalte `translator_id` herzustellen, geben Sie einen optionalen Parameter im Verknüpfungsausdruck an. +Sie können auch einen Callback als Parameter übergeben, der entweder den Wert selbst oder ein Schlüssel-Wert-Paar für jede Zeile zurückgibt. Wenn der Callback nur einen Wert zurückgibt, ist der Schlüssel der Primärschlüssel der Zeile: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Erstes Buch (Jan Novak)', ...] + +// Der Callback kann auch ein Array mit einem Schlüssel- und Wertepaar zurückgeben: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Erstes Buch' => 'Jan Novak', ...] ``` -Werfen wir einen Blick auf einige schwierigere Verknüpfungsausdrücke. -Wir möchten alle Autoren finden, die etwas über PHP geschrieben haben. Alle Bücher haben Tags, also sollten wir die Autoren auswählen, die ein Buch mit dem Tag PHP geschrieben haben. +fetchAll(): array .[method] +--------------------------- + +Gibt alle Zeilen als assoziatives Array von `ActiveRow` Objekten zurück, wobei die Schlüssel die Primärschlüsselwerte sind. ```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, ...), ...] ``` -Aggregierte Abfragen .[#toc-aggregate-queries] ----------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Anzahl der Zeilen ermitteln -| `$table->count("DISTINCT $column")` | Ermittelt die Anzahl der eindeutigen Werte -| `$table->min($column)` | Ermittelt den Mindestwert -| `$table->max($column)` | Ermittelt den Maximalwert -| `$table->sum($column)` | Ermittelt die Summe aller Werte -| `$table->aggregation("GROUP_CONCAT($column)")` | Ausführen einer beliebigen Aggregationsfunktion +Die Methode `count()` ohne Parameter gibt die Anzahl der Zeilen im Objekt `Selection` zurück: -.[caution] -Die Methode `count()` ohne Angabe von Parametern wählt alle Datensätze aus und gibt die Array-Größe zurück, was sehr ineffizient ist. Wenn Sie zum Beispiel die Anzahl der Zeilen für das Paging berechnen müssen, geben Sie immer das erste Argument an. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativ +``` +Hinweis: `count()` mit einem Parameter führt die COUNT-Aggregationsfunktion in der Datenbank aus, wie unten beschrieben. -Escaping & Quoting .[#toc-escaping-quoting] -=========================================== -Database Explorer ist intelligent und bricht Parameter und Anführungszeichen für Sie ab. Diese Grundregeln müssen jedoch befolgt werden: +ActiveRow::toArray(): array .[method] +------------------------------------- -- Schlüsselwörter, Funktionen und Prozeduren müssen in Großbuchstaben geschrieben werden -- Spalten und Tabellen müssen klein geschrieben werden -- Übergabe von Variablen als Parameter, keine Verkettung +Konvertiert das Objekt `ActiveRow` in ein assoziatives Array, wobei die Schlüssel Spaltennamen und die Werte die entsprechenden Daten sind. ```php -->where('name like ?', 'John'); // FALSCH! generates: `name` `like` ? -->where('name LIKE ?', 'John'); // RICHTIG +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray wird ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + -->where('KEY = ?', $value); // FALSCH! KEY is a keyword -->where('key = ?', $value); // RICHTIG. generates: `key` = ? +Aggregation .[#toc-aggregation] +=============================== -->where('name = ' . $name); // FALSCH! sql injection! -->where('name = ?', $name); // RICHTIG +Die Klasse `Selection` bietet Methoden zur einfachen Durchführung von Aggregationsfunktionen (COUNT, SUM, MIN, MAX, AVG, etc.). -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // FALSCH! Variablen als Parameter übergeben, nicht verketten -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // RICHTIG +.[language-php] +| `count($expr)` | Zählt die Anzahl der Zeilen | +| `min($expr)` | Gibt den Minimalwert in einer Spalte zurück +| `max($expr)` | Liefert den Maximalwert einer Spalte | +| `sum($expr)` | Gibt die Summe der Werte in einer Spalte zurück | +| `aggregation($function)` | Erlaubt jede Aggregationsfunktion, wie `AVG()` oder `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Führt eine SQL-Abfrage mit der Funktion COUNT aus und gibt das Ergebnis zurück. Diese Methode wird verwendet, um festzustellen, wie viele Zeilen eine bestimmte Bedingung erfüllen: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `Tabelle` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `Spalte`) FROM `Tabelle` +``` + +Hinweis: [count() |#count()] ohne Parameter gibt einfach die Anzahl der Zeilen im Objekt `Selection` zurück. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Die Methoden `min()` und `max()` geben die Minimal- und Maximalwerte in der angegebenen Spalte oder dem Ausdruck zurück: + +```php +// SELECT MAX(`Preis`) FROM `Produkte` WHERE `aktiv` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Gibt die Summe der Werte in der angegebenen Spalte oder dem angegebenen Ausdruck zurück: + +```php +// SELECT SUM(`Preis` * `Artikel_im_Bestand`) FROM `Produkte` WHERE `aktiv` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` + + +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Erlaubt die Ausführung einer beliebigen Aggregationsfunktion. + +```php +// Berechnet den Durchschnittspreis der Produkte in einer Kategorie +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Kombiniert Produkt-Tags zu einer einzigen Zeichenfolge +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Wenn wir Ergebnisse aggregieren müssen, die selbst aus einer Aggregation und Gruppierung resultieren (z. B. `SUM(value)` über gruppierte Zeilen), geben wir die Aggregationsfunktion, die auf diese Zwischenergebnisse angewendet werden soll, als zweites Argument an: + +```php +// Berechnet den Gesamtpreis der auf Lager befindlichen Produkte für jede Kategorie und summiert dann diese Preise +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); ``` -.[warning] -Falsche Verwendung kann zu Sicherheitslücken führen +In diesem Beispiel berechnen wir zunächst den Gesamtpreis der Produkte in jeder Kategorie (`SUM(price * stock) AS category_total`) und gruppieren die Ergebnisse nach `category_id`. Anschließend verwenden wir `aggregation('SUM(category_total)', 'SUM')`, um diese Zwischensummen zu summieren. Das zweite Argument `'SUM'` gibt die Aggregationsfunktion an, die auf die Zwischenergebnisse anzuwenden ist. -Abrufen von Daten .[#toc-fetching-data] -======================================= +Einfügen, Aktualisieren und Löschen .[#toc-insert-update-delete] +================================================================ -| `foreach ($table as $id => $row)` | Iterieren über alle Zeilen im Ergebnis -| `$row = $table->get($id)` | Einzelne Zeile mit ID $id aus Tabelle holen -| `$row = $table->fetch()` | Holt die nächste Zeile aus dem Ergebnis -| `$array = $table->fetchPairs($key, $value)` | Holt alle Werte in ein assoziatives Array -| `$array = $table->fetchPairs($value)` | Holt alle Zeilen in ein assoziatives Array -| `count($table)` | Anzahl der Zeilen in der Ergebnismenge ermitteln +Der Nette Database Explorer vereinfacht das Einfügen, Aktualisieren und Löschen von Daten. Alle genannten Methoden werfen im Fehlerfall eine `Nette\Database\DriverException` aus. -Einfügen, Aktualisieren & Löschen .[#toc-insert-update-delete] -============================================================== +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- -Die Methode `insert()` akzeptiert ein Array von Traversable-Objekten (z. B. [ArrayHash |utils:arrays#ArrayHash], das [Formulare |forms:] zurückgibt): +Fügt neue Datensätze in eine Tabelle ein. + +**Einfügen eines einzelnen Datensatzes:** + +Der neue Datensatz wird als assoziatives Array oder iterierbares Objekt (wie `ArrayHash`, das in [Formularen |forms:] verwendet wird) übergeben, wobei die Schlüssel mit den Spaltennamen in der Tabelle übereinstimmen. + +Wenn die Tabelle einen definierten Primärschlüssel hat, gibt die Methode ein `ActiveRow` -Objekt zurück, das aus der Datenbank neu geladen wird, um alle auf Datenbankebene vorgenommenen Änderungen widerzuspiegeln (z. B. Trigger, Standardspaltenwerte oder automatische Inkrementberechnungen). Dadurch wird die Datenkonsistenz gewährleistet, und das Objekt enthält immer die aktuellen Datenbankdaten. Wenn ein Primärschlüssel nicht explizit definiert ist, gibt die Methode die Eingabedaten als Array zurück. ```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 ist eine Instanz von ActiveRow, die die vollständigen Daten der eingefügten Zeile enthält, +// einschließlich der automatisch erzeugten ID und aller durch Trigger vorgenommenen Änderungen +echo $row->id; // Gibt die ID des neu eingefügten Benutzers aus +echo $row->created_at; // Gibt die Erstellungszeit aus, wenn sie durch einen Trigger festgelegt wurde ``` -Wenn der Primärschlüssel in der Tabelle definiert ist, wird ein ActiveRow-Objekt zurückgegeben, das die eingefügte Zeile enthält. +**Einfügen mehrerer Datensätze auf einmal:** -Mehrfaches Einfügen: +Mit der Methode `insert()` können Sie mit einer einzigen SQL-Abfrage mehrere Datensätze einfügen. In diesem Fall gibt sie die Anzahl der eingefügten Zeilen zurück. ```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 `Benutzer` (`Name`, `Jahr`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows wird 2 sein ``` -Dateien oder DateTime-Objekte können als Parameter übergeben werden: +Sie können auch ein `Selection` Objekt mit einer Auswahl von Daten als Parameter übergeben. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Einfügen spezieller Werte:** + +Die Werte können Dateien, `DateTime` Objekte oder SQL-Literale enthalten: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // konvertiert in das Datenbankformat + 'avatar' => fopen('image.jpg', 'rb'), // fügt den Inhalt der Binärdatei ein + 'uuid' => $explorer::literal('UUID()'), // ruft die Funktion UUID() auf ]); ``` -Aktualisieren (gibt die Anzahl der betroffenen Zeilen zurück): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Aktualisiert Zeilen in einer Tabelle basierend auf einem angegebenen Filter. Gibt die Anzahl der tatsächlich geänderten Zeilen zurück. + +Die zu aktualisierenden Spalten werden als assoziatives Array oder iterierbares Objekt (wie `ArrayHash`, das in [Formularen |forms:] verwendet wird) übergeben, wobei die Schlüssel mit den Spaltennamen in der Tabelle übereinstimmen: ```php -$count = $explorer->table('users') - ->where('id', 10) // muss vor update() aufgerufen werden +$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 `Benutzer` SET `Name` = 'John Smith', `Jahr` = 1994 WHERE `ID` = 10 ``` -Für die Aktualisierung können wir die Operatoren `+=` und `-=` verwenden: +Um numerische Werte zu ändern, können Sie die Operatoren `+=` und `-=` verwenden: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // siehe += + 'points+=' => 1, // erhöht den Wert der Spalte "Punkte" um 1 + 'coins-=' => 1, // verringert den Wert der Spalte "Münzen" um 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `Benutzer` SET `Punkte` = `Punkte` + 1, `Münzen` = `Münzen` - 1 WHERE `ID` = 10 ``` -Löschen (gibt die Anzahl der gelöschten Zeilen zurück): + +Selection::delete(): int .[method] +---------------------------------- + +Löscht Zeilen aus einer Tabelle basierend auf einem angegebenen Filter. Gibt die Anzahl der gelöschten Zeilen zurück. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `Benutzer` WHERE `id` = 10 ``` +.[caution] +Wenn Sie `update()` oder `delete()` aufrufen, müssen Sie `where()` verwenden, um die zu aktualisierenden oder zu löschenden Zeilen anzugeben. Wenn `where()` nicht verwendet wird, wird der Vorgang für die gesamte Tabelle durchgeführt! + -Arbeiten mit Relationen .[#toc-working-with-relationships] -========================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Aktualisiert die Daten in einer Datenbankzeile, die durch das Objekt `ActiveRow` dargestellt wird. Es akzeptiert iterierbare Daten als Parameter, wobei die Schlüssel Spaltennamen sind. Um numerische Werte zu ändern, können Sie die Operatoren `+=` und `-=` verwenden: -Hat eine Beziehung .[#toc-has-one-relation] -------------------------------------------- -Hat eine Beziehung ist ein häufiger Anwendungsfall. Buch *hat einen* Autor. Ein Buch *hat einen* Übersetzer. Das Abrufen von verwandten Zeilen wird hauptsächlich mit der Methode `ref()` durchgeführt. Sie akzeptiert zwei Argumente: den Namen der Zieltabelle und die Spalte der Quellverbindung. Siehe Beispiel: +Nach der Aktualisierung wird `ActiveRow` automatisch neu aus der Datenbank geladen, um alle auf Datenbankebene vorgenommenen Änderungen (z. B. Trigger) zu berücksichtigen. Die Methode gibt `true` nur zurück, wenn eine echte Datenänderung stattgefunden hat. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // Erhöht die Anzahl der Ansichten +]); +echo $article->views; // Gibt die aktuelle Anzahl der Ansichten aus ``` -Im obigen Beispiel holen wir einen verwandten Autoreneintrag aus der Tabelle `author`, der Primärschlüssel des Autors wird über die Spalte `book.author_id` gesucht. Die Methode Ref() gibt eine ActiveRow-Instanz zurück oder null, wenn es keinen entsprechenden Eintrag gibt. Die zurückgegebene Zeile ist eine Instanz von ActiveRow, so dass wir mit ihr auf dieselbe Weise arbeiten können wie mit dem Bucheintrag. +Diese Methode aktualisiert nur eine bestimmte Zeile in der Datenbank. Für Massenaktualisierungen von mehreren Zeilen verwenden Sie die Methode [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Löscht eine Zeile aus der Datenbank, die durch das Objekt `ActiveRow` repräsentiert wird. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Löscht das Buch mit der ID 1 +``` + +Diese Methode löscht nur eine bestimmte Zeile in der Datenbank. Für das Massenlöschen von mehreren Zeilen verwenden Sie die Methode [Selection::delete() |#Selection::delete()]. -// or directly -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +Beziehungen zwischen Tabellen .[#toc-relationships-between-tables] +================================================================== + +In relationalen Datenbanken sind die Daten in mehrere Tabellen aufgeteilt und durch Fremdschlüssel miteinander verbunden. Nette Database Explorer bietet eine revolutionäre Möglichkeit, mit diesen Beziehungen zu arbeiten - ohne JOIN-Abfragen zu schreiben oder irgendeine Konfiguration oder Entitätserzeugung zu benötigen. + +Zur Demonstration verwenden wir die **Beispieldatenbank**[(verfügbar auf GitHub |https://github.com/nette-examples/books]). Die Datenbank enthält die folgenden Tabellen: + +- `author` - Autoren und Übersetzer (Spalten `id`, `name`, `web`, `born`) +- `book` - Bücher (Spalten `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - Tags (Spalten `id`, `name`) +- `book_tag` - Verknüpfungstabelle zwischen Büchern und Tags (Spalten `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Datenbankstruktur .<> + +In diesem Beispiel einer Buchdatenbank finden wir mehrere Arten von Beziehungen (vereinfacht im Vergleich zur Realität): + +- **Eine-zu-viele (1:N)** - Jedes Buch **hat einen** Autor; ein Autor kann **mehrere** Bücher schreiben. +- Zero-to-many (0:N)** - Ein Buch **kann** einen Übersetzer haben; ein Übersetzer kann **mehrere** Bücher übersetzen. +- **Null-zu-Eins (0:1)** - Ein Buch **kann** eine Fortsetzung haben. +- **Many-to-many (M:N)** - Ein Buch **kann mehrere** Tags haben, und ein Tag kann **mehreren** Büchern zugewiesen werden. + +In diesen Beziehungen gibt es immer eine **Elterntabelle** und eine **Kindertabelle**. In der Beziehung zwischen Autoren und Büchern ist zum Beispiel die Tabelle `author` die Elterntabelle und die Tabelle `book` das Kind - man kann sich das so vorstellen, dass ein Buch immer zu einem Autor "gehört". Dies spiegelt sich auch in der Datenbankstruktur wider: Die untergeordnete Tabelle `book` enthält den Fremdschlüssel `author_id`, der auf die übergeordnete Tabelle `author` verweist. + +Wenn wir die Bücher zusammen mit den Namen ihrer Autoren anzeigen wollen, haben wir zwei Möglichkeiten. Entweder wir rufen die Daten über eine einzige SQL-Abfrage mit einem JOIN ab: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Oder wir rufen die Daten in zwei Schritten ab - erst die Bücher, dann die Autoren - und stellen sie in PHP zusammen: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Book hat auch einen Übersetzer, so dass der Name des Übersetzers recht einfach zu ermitteln ist. +Der zweite Ansatz ist überraschenderweise **effizienter**. Die Daten werden nur einmal abgerufen und können im Cache besser genutzt werden. Genau so funktioniert der Nette Database Explorer - er erledigt alles unter der Haube und bietet Ihnen eine saubere 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 ist ein Datensatz aus der Tabelle "author". + echo 'translated by: ' . $book->translator?->name; +} ``` -All das ist gut, aber etwas umständlich, finden Sie nicht auch? Der Datenbank-Explorer enthält bereits die Definitionen der Fremdschlüssel, warum sie also nicht automatisch verwenden? Lassen Sie uns das tun! -Wenn wir eine Eigenschaft aufrufen, die nicht existiert, versucht ActiveRow, den Namen der aufrufenden Eigenschaft als 'hat eine' Beziehung aufzulösen. Das Abrufen dieser Eigenschaft ist dasselbe wie der Aufruf der ref()-Methode mit nur einem Argument. Wir nennen das einzige Argument **key**. Der Schlüssel wird in eine bestimmte Fremdschlüsselbeziehung aufgelöst. Der übergebene Schlüssel wird mit Zeilenspalten abgeglichen, und wenn er übereinstimmt, wird der in der entsprechenden Spalte definierte Fremdschlüssel verwendet, um Daten aus der zugehörigen Zieltabelle zu erhalten. Siehe Beispiel: +Zugriff auf die übergeordnete Tabelle .[#toc-accessing-the-parent-table] +------------------------------------------------------------------------ + +Der Zugriff auf die übergeordnete Tabelle ist ganz einfach. Es handelt sich um Beziehungen wie *ein Buch hat einen Autor* oder *ein Buch kann einen Übersetzer haben*. Auf den Bezugsdatensatz kann über die Objekteigenschaft `ActiveRow` zugegriffen werden - der Eigenschaftsname entspricht dem Spaltennamen des Fremdschlüssels ohne das Suffix `id`: ```php -$book->author->name; -// gleich wie -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // findet den Autor über die Spalte 'author_id' +echo $book->translator?->name; // findet den Übersetzer über die Spalte 'translator_id'. ``` -Die ActiveRow-Instanz hat keine Autorenspalte. Alle Buchspalten werden nach einer Übereinstimmung mit *key* durchsucht. Übereinstimmung bedeutet in diesem Fall, dass der Spaltenname den Schlüssel enthalten muss. Im obigen Beispiel enthält die Spalte `author_id` die Zeichenfolge "author" und wird daher mit dem Schlüssel "author" abgeglichen. Wenn Sie den Buchübersetzer abrufen möchten, können Sie z. B. "Übersetzer" als Schlüssel verwenden, da der Schlüssel "Übersetzer" mit der Spalte `translator_id` übereinstimmt. Mehr über die Logik der Schlüsselübereinstimmung finden Sie im Kapitel [Verknüpfung von Ausdrücken |#joining-key]. +Beim Zugriff auf die Eigenschaft `$book->author` sucht der Explorer nach einer Spalte in der Tabelle `book`, die die Zeichenfolge `author` enthält (d. h. `author_id`). Basierend auf dem Wert in dieser Spalte ruft er den entsprechenden Datensatz aus der Tabelle `author` ab und gibt ihn als `ActiveRow` Objekt zurück. In ähnlicher Weise verwendet `$book->translator` die Spalte `translator_id`. Da die Spalte `translator_id` `null` enthalten kann, wird der Operator `?->` verwendet. + +Einen alternativen Ansatz bietet die Methode `ref()`, die zwei Argumente akzeptiert - den Namen der Zieltabelle und die Verknüpfungsspalte - und eine `ActiveRow` Instanz oder `null` zurückgibt: ```php -echo $book->title . ': '; -echo $book->author->name; -if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; -} +echo $book->ref('author', 'author_id')->name; // Link zum Autor +echo $book->ref('author', 'translator_id')->name; // Link zum Übersetzer ``` -Wenn Sie mehrere Bücher abrufen möchten, sollten Sie den gleichen Ansatz verwenden. Nette Database Explorer wird die Autoren und Übersetzer für alle abgerufenen Bücher auf einmal abrufen. +Die Methode `ref()` ist nützlich, wenn der eigenschaftsbasierte Zugriff nicht verwendet werden kann, z. B. wenn die Tabelle eine Spalte mit demselben Namen wie die Eigenschaft enthält (`author`). In anderen Fällen wird die Verwendung des eigenschaftsbasierten Zugriffs zur besseren Lesbarkeit empfohlen. + +Der Explorer optimiert automatisch die Datenbankabfragen. Bei der Iteration durch Bücher und dem Zugriff auf die zugehörigen Datensätze (Autoren, Übersetzer) erstellt der Explorer nicht für jedes Buch eine eigene Abfrage. Stattdessen führt er nur **eine SELECT-Abfrage für jede Art von Beziehung** aus, was die Datenbankbelastung erheblich reduziert. Ein Beispiel: ```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; } ``` -Der Code wird nur diese 3 Abfragen ausführen: +Dieser Code führt nur drei optimierte Datenbankabfragen aus: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Die Logik zur Identifizierung der Verknüpfungsspalte wird durch die Implementierung von [Conventions |api:Nette\Database\Conventions] definiert. Wir empfehlen die Verwendung von [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], das Fremdschlüssel analysiert und es Ihnen ermöglicht, nahtlos mit bestehenden Tabellenbeziehungen zu arbeiten. -Hat viele Relationen .[#toc-has-many-relation] ----------------------------------------------- -Die Beziehung "hat viele" ist einfach die Umkehrung der Beziehung "hat einen". Autor *hat* *viele* Bücher geschrieben. Der Autor *hat* *viele* Bücher übersetzt. Wie Sie sehen können, ist diese Art von Beziehung etwas schwieriger, weil die Beziehung 'benannt' ist ('geschrieben', 'übersetzt'). Die ActiveRow-Instanz verfügt über die Methode `related()`, die ein Array mit verwandten Einträgen zurückgibt. Die Einträge sind ebenfalls ActiveRow Instanzen. Siehe Beispiel unten: +Zugriff auf die Child-Tabelle .[#toc-accessing-the-child-table] +--------------------------------------------------------------- + +Der Zugriff auf die untergeordnete Tabelle funktioniert in umgekehrter Richtung. Jetzt fragen wir *welche Bücher hat dieser Autor geschrieben* oder *welche Bücher hat dieser Übersetzer übersetzt*. Für diese Art von Abfrage verwenden wir die Methode `related()`, die ein `Selection` Objekt mit Bezugsdatensätzen zurückgibt. Hier ist ein Beispiel: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Gibt alle Bücher aus, die der Autor geschrieben hat foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Gibt alle vom Autor übersetzten Bücher aus foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Methode `related()` Methode akzeptiert eine vollständige Join-Beschreibung, die als zwei Argumente oder als ein durch Punkt verbundenes Argument übergeben wird. Das erste Argument ist die Zieltabelle, das zweite ist die Zielspalte. +Die Methode `related()` akzeptiert die Beziehungsbeschreibung als ein einziges Argument in Punktschreibweise oder als zwei separate Argumente: ```php -$author->related('book.translator_id'); -// gleich wie -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // ein Argument +$author->related('book', 'translator_id'); // zwei Argumente ``` -Sie können die auf Fremdschlüsseln basierende Heuristik des Nette Database Explorer verwenden und nur das Argument **key** übergeben. Key wird mit allen Fremdschlüsseln abgeglichen, die auf die aktuelle Tabelle (`author` table) zeigen. Wenn es eine Übereinstimmung gibt, wird Nette Database Explorer diesen Fremdschlüssel verwenden, andernfalls wird [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] oder [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] ausgelöst. Mehr über die Logik der Schlüsselübereinstimmung finden Sie im Kapitel [Joining-Ausdrücke |#joining-key]. +Der Explorer kann automatisch die richtige Verknüpfungsspalte anhand des Namens der übergeordneten Tabelle erkennen. In diesem Fall erfolgt die Verknüpfung über die Spalte `book.author_id`, da der Name der Quelltabelle `author` lautet: -Natürlich können Sie die entsprechenden Methoden für alle abgerufenen Autoren aufrufen, der Nette Database Explorer wird dann die entsprechenden Bücher auf einmal abrufen. +```php +$author->related('book'); // verwendet book.author_id +``` + +Wenn mehrere mögliche Verbindungen bestehen, löst der Explorer eine Ausnahme [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] aus. + +Wir können natürlich auch die Methode `related()` verwenden, wenn wir durch mehrere Datensätze in einer Schleife iterieren, und der Explorer wird die Abfragen auch in diesem Fall automatisch optimieren: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Im obigen Beispiel werden nur zwei Abfragen ausgeführt: +Dieser Code erzeugt nur zwei effiziente SQL-Abfragen: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors +``` + + +Many-to-Many-Beziehung .[#toc-many-to-many-relationship] +-------------------------------------------------------- + +Für eine Many-to-Many-Beziehung (M:N) ist eine **Verzweigungstabelle** (in unserem Fall `book_tag`) erforderlich. Diese Tabelle enthält zwei Fremdschlüsselspalten (`book_id`, `tag_id`). Jede Spalte verweist auf den Primärschlüssel einer der verbundenen Tabellen. Um Bezugsdaten abzurufen, holen wir zunächst Datensätze aus der Verknüpfungstabelle mit `related('book_tag')` und fahren dann mit den Zieldaten fort: + +```php +$book = $explorer->table('book')->get(1); +// Gibt die Namen der dem Buch zugewiesenen Tags aus +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // holt sich den Tag-Namen über die Link-Tabelle +} + +$tag = $explorer->table('tag')->get(1); +// Umgekehrte Richtung: gibt die Titel der Bücher mit diesem Tag aus +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // holt den Buchtitel +} +``` + +Der Explorer optimiert die SQL-Abfragen erneut in eine effiziente Form: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag ``` -Manuelles Erstellen des Explorers .[#toc-creating-explorer-manually] -==================================================================== +Abfragen über Bezugstabellen .[#toc-querying-through-related-tables] +-------------------------------------------------------------------- -Eine Datenbankverbindung kann über die Anwendungskonfiguration erstellt werden. In diesem Fall wird ein `Nette\Database\Explorer` Dienst erstellt, der über den DI-Container als Abhängigkeit übergeben werden kann. +In den Methoden `where()`, `select()`, `order()` und `group()` können Sie spezielle Notationen verwenden, um auf Spalten aus anderen Tabellen zuzugreifen. Der Explorer erstellt automatisch die erforderlichen JOINs. -Wenn der Nette Database Explorer jedoch als eigenständiges Tool verwendet wird, muss eine Instanz des `Nette\Database\Explorer` Objekts manuell erstellt werden. +Die **Punktnotation** (`parent_table.column`) wird für 1:N-Beziehungen aus der Perspektive der übergeordneten Tabelle verwendet: ```php -// $storage implementiert Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Findet Bücher, deren Autorennamen mit 'Jon' beginnen +$books->where('author.name LIKE ?', 'Jon%'); + +// Sortiert die Bücher nach Autorennamen absteigend +$books->order('author.name DESC'); + +// Gibt Buchtitel und Autorennamen aus +$books->select('book.title, author.name'); +``` + +Die **Kolon-Notation** wird für 1:N-Beziehungen aus der Sicht der übergeordneten Tabelle verwendet: + +```php +$authors = $explorer->table('author'); + +// Findet Autoren, die ein Buch mit 'PHP' im Titel geschrieben haben +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Zählt die Anzahl der Bücher für jeden Autor +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Im obigen Beispiel mit Doppelpunkt-Notation (`:book.title`) wird die Fremdschlüsselspalte nicht explizit angegeben. Der Explorer erkennt automatisch die richtige Spalte anhand des Namens der übergeordneten Tabelle. In diesem Fall erfolgt die Verbindung über die Spalte `book.author_id`, da der Name der Quelltabelle `author` lautet. Wenn mehrere mögliche Verbindungen bestehen, löst Explorer die Ausnahme [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] aus. + +Die Verknüpfungsspalte kann explizit in Klammern angegeben werden: + +```php +// Findet Autoren, die ein Buch mit 'PHP' im Titel übersetzt haben +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Notationen können verkettet werden, um auf Daten über mehrere Tabellen hinweg zuzugreifen: + +```php +// Findet Autoren von Büchern, die mit 'PHP' getaggt sind +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Bedingungen für JOIN verlängern .[#toc-extending-conditions-for-join] +--------------------------------------------------------------------- + +Die Methode `joinWhere()` fügt zusätzliche Bedingungen zu Tabellen-Joins in SQL nach dem Schlüsselwort `ON` hinzu. + +Nehmen wir zum Beispiel an, wir wollen Bücher finden, die von einem bestimmten Übersetzer übersetzt wurden: + +```php +// Findet Bücher, die von einem Übersetzer namens 'David' übersetzt wurden +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +In der Bedingung `joinWhere()` können Sie die gleichen Konstrukte wie in der Methode `where()` verwenden - Operatoren, Platzhalter, Wertefelder oder SQL-Ausdrücke. + +Für komplexere Abfragen mit mehreren JOINs können Tabellen-Aliase definiert werden: + +```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 (`buch_autor`.`geboren` < 1950) +``` + +Beachten Sie, dass die Methode `where()` der Klausel `WHERE` Bedingungen hinzufügt, während die Methode `joinWhere()` die Bedingungen in der Klausel `ON` bei Tabellen-Joins erweitert. + + +Manuelles Erstellen von Explorer .[#toc-manually-creating-explorer] +=================================================================== + +Wenn Sie nicht den Nette-DI-Container verwenden, können Sie eine Instanz von `Nette\Database\Explorer` manuell erstellen: + +```php +use Nette\Database; + +// $storage implementiert Nette\Caching\Storage, z. B.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// Datenbankverbindung +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// verwaltet die Reflexion der Datenbankstruktur +$structure = new Database\Structure($connection, $storage); +// definiert Regeln für das Mapping von Tabellennamen, Spalten und Fremdschlüsseln +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/de/security.texy b/database/de/security.texy new file mode 100644 index 0000000000..2481dd3a3c --- /dev/null +++ b/database/de/security.texy @@ -0,0 +1,145 @@ +Sicherheitsrisiken +****************** + +.[perex] +Datenbanken enthalten oft sensible Daten und ermöglichen gefährliche Operationen. Nette Database bietet eine Reihe von Sicherheitsfunktionen. Es ist jedoch wichtig, den Unterschied zwischen sicheren und unsicheren APIs zu verstehen. + + +SQL-Einschleusung .[#toc-sql-injection] +======================================= + +SQL-Injection ist das größte Sicherheitsrisiko bei der Arbeit mit Datenbanken. Sie tritt auf, wenn ungeprüfte Benutzereingaben Teil einer SQL-Abfrage werden. Ein Angreifer kann seine eigenen SQL-Befehle einschleusen und so Daten in der Datenbank erlangen oder verändern. + +```php +// ❌ UNSAFE CODE - anfällig für SQL-Injection +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Der Angreifer kann etwas eingeben wie: ' OR '1'='1 +// Die resultierende Abfrage wird sein: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Dies gibt alle Benutzer zurück! +``` + +Das Gleiche gilt für den Database Explorer: + +```php +// ❌ UNSAFE CODE +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Sichere parametrisierte Abfragen .[#toc-safe-parameterized-queries] +=================================================================== + +Der sichere Weg, Werte in SQL-Abfragen einzufügen, sind parametrisierte Abfragen. Nette Database bietet mehrere Möglichkeiten, diese zu verwenden. + + +Platzhalter Fragezeichen .[#toc-placeholder-question-marks] +----------------------------------------------------------- + +Die einfachste Methode ist die Verwendung von Platzhalterfragezeichen: + +```php +// ✅ Sichere parametrisierte Abfragen +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Sichere Bedingung im Explorer +$table->where('name = ?', $_GET['name']); +``` + +Das Gleiche gilt für alle anderen Methoden im Database Explorer, die das Einfügen von Ausdrücken mit Platzhalterfragezeichen und Parametern erlauben. + +.[warning] +Werte müssen vom skalaren Typ sein (`string`, `int`, `float`, `bool`) oder `null`. Wenn z.B., `$_GET['name']` ein Array ist, wird Nette Database alle seine Elemente in die SQL-Abfrage aufnehmen, was unerwünscht sein kann. + + +Wert-Arrays .[#toc-value-arrays] +-------------------------------- + +Für die Klauseln `INSERT`, `UPDATE` oder `WHERE` können wir Wertefelder verwenden: + +```php +// ✅ Sicheres INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Sicheres UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database entschlüsselt automatisch alle Werte, die durch parametrisierte Abfragen übergeben werden. Allerdings muss der korrekte Datentyp der Parameter sichergestellt werden. + + +Array-Schlüssel sind keine sichere API .[#toc-array-keys-are-not-a-safe-api] +============================================================================ + +Während Werte in Arrays sicher sind, gilt dies nicht für Schlüssel: + +```php +// ❌ UNSAFE CODE - Schlüssel können SQL-Injection enthalten +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Für die Befehle `INSERT` und `UPDATE` ist dies eine kritische Sicherheitslücke - ein Angreifer könnte jede Spalte in der Datenbank einfügen oder verändern. Zum Beispiel könnte er `is_admin = 1` setzen oder beliebige Daten in sensible Spalten einfügen. + +Unter den Bedingungen von `WHERE` ist dies sogar noch gefährlicher, weil es **SQL enumeration** erlaubt - eine Technik, um schrittweise Informationen über die Datenbank abzurufen. Ein Angreifer könnte versuchen, die Gehälter von Mitarbeitern zu erkunden, indem er `$_GET` auf diese Weise infiltriert: + +```php +$_GET = ['salary >', 100000]; // beginnt mit der Festlegung von Gehaltsspannen +``` + +Das Hauptproblem ist jedoch, dass `WHERE` Bedingungen SQL-Ausdrücke in Schlüsseln unterstützen: + +```php +// Legitime Verwendung von Operatoren in Schlüsseln +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// UNSAFE: Angreifer kann sein eigenes SQL einfügen +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // ermöglicht dem Angreifer, Admin-Gehälter zu erhalten +``` + +Dies ist wieder einmal **SQL-Injektion**. + + +Whitelisting-Spalten .[#toc-whitelisting-columns] +------------------------------------------------- + +Wenn Sie Benutzern die Auswahl von Spalten erlauben wollen, verwenden Sie immer eine Whitelist: + +```php +// ✅ Sichere Verarbeitung - nur erlaubte Spalten +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dynamische Bezeichner .[#toc-dynamic-identifiers] +================================================= + +Für dynamische Tabellen- und Spaltennamen verwenden Sie den Platzhalter `?name`: + +```php +// ✅ Sichere Verwendung von vertrauenswürdigen Identifikatoren +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - niemals Benutzereingaben verwenden +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Das Symbol `?name` sollte nur für vertrauenswürdige Werte verwendet werden, die im Anwendungscode definiert sind. Für vom Benutzer bereitgestellte Werte verwenden Sie wiederum eine Whitelist. diff --git a/database/el/@left-menu.texy b/database/el/@left-menu.texy index 6a1070cb7f..b5f9c041bb 100644 --- a/database/el/@left-menu.texy +++ b/database/el/@left-menu.texy @@ -4,3 +4,4 @@ - [Εξερευνητής |Explorer] - [Αναστοχασμός |Reflection] - [Διαμόρφωση |Configuration] +- [Κίνδυνοι ασφαλείας |security] diff --git a/database/el/explorer.texy b/database/el/explorer.texy index b0dc43b07f..e1f50b61f5 100644 --- a/database/el/explorer.texy +++ b/database/el/explorer.texy @@ -3,548 +3,927 @@
-Ο Nette Database Explorer απλοποιεί σημαντικά την ανάκτηση δεδομένων από τη βάση δεδομένων χωρίς τη συγγραφή ερωτημάτων SQL. +Ο Nette Database Explorer είναι ένα ισχυρό επίπεδο που απλοποιεί σημαντικά την ανάκτηση δεδομένων από τη βάση δεδομένων χωρίς την ανάγκη συγγραφής ερωτημάτων SQL. -- χρησιμοποιεί αποδοτικά ερωτήματα -- δεν μεταδίδονται άσκοπα δεδομένα -- διαθέτει κομψή σύνταξη +- Η εργασία με τα δεδομένα είναι φυσική και εύκολα κατανοητή +- Δημιουργεί βελτιστοποιημένα ερωτήματα SQL που αντλούν μόνο τα απαραίτητα δεδομένα +- Παρέχει εύκολη πρόσβαση σε συναφή δεδομένα χωρίς την ανάγκη συγγραφής ερωτημάτων JOIN +- Λειτουργεί άμεσα χωρίς καμία διαμόρφωση ή δημιουργία οντοτήτων
-Για να χρησιμοποιήσετε την Εξερεύνηση βάσης δεδομένων, ξεκινήστε με έναν πίνακα - καλέστε το `table()` σε ένα αντικείμενο [api:Nette\Database\Explorer]. Ο ευκολότερος τρόπος για να λάβετε μια περίπτωση αντικειμένου πλαισίου [περιγράφεται εδώ |core#Connection and Configuration], ή, για την περίπτωση που ο Nette Database Explorer χρησιμοποιείται ως αυτόνομο εργαλείο, μπορεί να [δημιουργηθεί χειροκίνητα |#Creating Explorer Manually]. +Το Nette Database Explorer είναι μια επέκταση του χαμηλού επιπέδου [Nette Database Core |core] layer, το οποίο προσθέτει μια βολική αντικειμενοστραφή προσέγγιση στη διαχείριση βάσεων δεδομένων. + +Η εργασία με τον Explorer ξεκινά με την κλήση της μεθόδου `table()` στο αντικείμενο [api:Nette\Database\Explorer] (ο τρόπος απόκτησής του [περιγράφεται εδώ |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // Το όνομα του πίνακα db είναι 'book' +$books = $explorer->table('book'); // 'book' είναι το όνομα του πίνακα ``` -Η κλήση επιστρέφει μια περίπτωση του αντικειμένου [Selection |api:Nette\Database\Table\Selection], η οποία μπορεί να επαναληφθεί για να ανακτηθούν όλα τα βιβλία. Κάθε στοιχείο (μια γραμμή) αναπαρίσταται από μια περίπτωση του [ActiveRow |api:Nette\Database\Table\ActiveRow] με δεδομένα που αντιστοιχίζονται στις ιδιότητές του: +Η μέθοδος επιστρέφει ένα αντικείμενο [Selection |api:Nette\Database\Table\Selection], το οποίο αναπαριστά ένα ερώτημα SQL. Πρόσθετες μέθοδοι μπορούν να συνδεθούν αλυσιδωτά με αυτό το αντικείμενο για το φιλτράρισμα και την ταξινόμηση των αποτελεσμάτων. Το ερώτημα συναρμολογείται και εκτελείται μόνο όταν ζητούνται τα δεδομένα, για παράδειγμα, με επανάληψη με το `foreach`. Κάθε γραμμή αντιπροσωπεύεται από ένα αντικείμενο [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // εξάγει τη στήλη 'title' + echo $book->author_id; // εξάγει τη στήλη 'author_id } ``` -Η απόκτηση μόνο μιας συγκεκριμένης γραμμής γίνεται με τη μέθοδο `get()`, η οποία επιστρέφει απευθείας μια περίπτωση ActiveRow. +Explorer απλοποιεί σημαντικά την εργασία με τις [σχέσεις των πινάκων |#Vazby mezi tabulkami]. Το ακόλουθο παράδειγμα δείχνει πόσο εύκολα μπορούμε να εξάγουμε δεδομένα από σχετικούς πίνακες (βιβλία και οι συγγραφείς τους). Παρατηρήστε ότι δεν χρειάζεται να γράψετε κανένα ερώτημα JOIN- η Nette τα παράγει για εμάς: ```php -$book = $explorer->table('book')->get(2); // επιστρέφει βιβλίο με id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // δημιουργεί ένα JOIN στον πίνακα 'author' +} ``` -Ας ρίξουμε μια ματιά σε μια κοινή περίπτωση χρήσης. Πρέπει να ανακτήσετε βιβλία και τους συγγραφείς τους. Πρόκειται για μια κοινή σχέση 1:N. Η συχνά χρησιμοποιούμενη λύση είναι η άντληση δεδομένων με τη χρήση ενός ερωτήματος SQL με ενώσεις πινάκων. Η δεύτερη δυνατότητα είναι να φέρνετε τα δεδομένα ξεχωριστά, να εκτελείτε ένα ερώτημα για τη λήψη βιβλίων και στη συνέχεια να παίρνετε έναν συγγραφέα για κάθε βιβλίο με ένα άλλο ερώτημα (π.χ. στον κύκλο foreach σας). Αυτό θα μπορούσε εύκολα να βελτιστοποιηθεί ώστε να εκτελούνται μόνο δύο ερωτήματα, ένα για τα βιβλία και ένα άλλο για τους απαιτούμενους συγγραφείς - και αυτός ακριβώς είναι ο τρόπος με τον οποίο το κάνει ο Nette Database Explorer. +Η Nette Database Explorer βελτιστοποιεί τα ερωτήματα για μέγιστη αποδοτικότητα. Το παραπάνω παράδειγμα εκτελεί μόνο δύο ερωτήματα SELECT, ανεξάρτητα από το αν επεξεργαζόμαστε 10 ή 10.000 βιβλία. -Στα παραδείγματα που ακολουθούν, θα εργαστούμε με το σχήμα της βάσης δεδομένων του σχήματος. Υπάρχουν σύνδεσμοι OneHasMany (1:N) (συγγραφέας του βιβλίου `author_id` και πιθανός μεταφραστής `translator_id`, ο οποίος μπορεί να είναι `null`) και σύνδεσμος ManyHasMany (M:N) μεταξύ του βιβλίου και των ετικετών του. +Επιπλέον, ο Explorer παρακολουθεί ποιες στήλες χρησιμοποιούνται στον κώδικα και αντλεί μόνο αυτές από τη βάση δεδομένων, εξοικονομώντας περαιτέρω απόδοση. Αυτή η συμπεριφορά είναι πλήρως αυτόματη και προσαρμοστική. Αν αργότερα τροποποιήσετε τον κώδικα για να χρησιμοποιήσετε επιπλέον στήλες, ο Explorer προσαρμόζει αυτόματα τα ερωτήματα. Δεν χρειάζεται να ρυθμίσετε τίποτα ή να σκεφτείτε ποιες στήλες θα χρειαστούν - αφήστε το στη Nette. -[Ένα παράδειγμα, συμπεριλαμβανομένου ενός σχήματος, βρίσκεται στο GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Δομή βάσης δεδομένων που χρησιμοποιείται στα παραδείγματα .<> +Φιλτράρισμα και ταξινόμηση .[#toc-filtering-and-sorting] +======================================================== -Ο παρακάτω κώδικας παραθέτει το όνομα του συγγραφέα για κάθε βιβλίο και όλες τις ετικέτες του. Θα [συζητήσουμε σε λίγο |#Working with relationships] πώς αυτό λειτουργεί εσωτερικά. +Η κλάση `Selection` παρέχει μεθόδους φιλτραρίσματος και ταξινόμησης δεδομένων. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Προσθέτει μια συνθήκη WHERE. Οι πολλαπλές συνθήκες συνδυάζονται με τη χρήση AND | +| `whereOr(array $conditions)` | Προσθέτει μια ομάδα συνθηκών WHERE που συνδυάζονται με τη χρήση OR | +| `wherePrimary($value)` | Προσθέτει μια συνθήκη WHERE με βάση το πρωτεύον κλειδί | +| `order($columns, ...$params)` | Ορίζει ταξινόμηση με ORDER BY | +| `select($columns, ...$params)` | Καθορίζει ποιες στήλες θα αντληθούν | +| `limit($limit, $offset = null)` | Περιορίζει τον αριθμό των γραμμών (LIMIT) και προαιρετικά ορίζει OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Ορίζει σελιδοποίηση | +| `group($columns, ...$params)` | Ομαδοποιεί σειρές (GROUP BY) | +| `having($condition, ...$params)`| Προσθέτει μια συνθήκη HAVING για το φιλτράρισμα των ομαδοποιημένων γραμμών | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author is row from table 'author' +Οι μέθοδοι μπορούν να αλυσιδωθούν (η λεγόμενη [ρευστή διεπαφή |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 is row from table 'tag' - } -} -``` +Αυτές οι μέθοδοι επιτρέπουν επίσης τη χρήση ειδικών συμβολισμών για την πρόσβαση σε [δεδομένα από σχετικούς πίνακες |#Dotazování přes související tabulky]. -Θα μείνετε ευχαριστημένοι από το πόσο αποτελεσματικά λειτουργεί το επίπεδο της βάσης δεδομένων. Το παραπάνω παράδειγμα πραγματοποιεί έναν σταθερό αριθμό αιτήσεων που μοιάζουν ως εξής: -```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)) -``` +Διαφυγή και αναγνωριστικά .[#toc-escaping-and-identifiers] +---------------------------------------------------------- -Αν χρησιμοποιείτε [κρυφή μνήμη |caching:] (προεπιλογή on), δεν θα ζητούνται άσκοπα στήλες. Μετά το πρώτο ερώτημα, η κρυφή μνήμη θα αποθηκεύσει τα χρησιμοποιούμενα ονόματα στηλών και ο Nette Database Explorer θα εκτελεί ερωτήματα μόνο με τις απαραίτητες στήλες: +Οι μέθοδοι αποφεύγουν αυτόματα τις παραμέτρους και τα αναγνωριστικά παραθέματος (ονόματα πινάκων και στηλών), αποτρέποντας την έγχυση SQL. Για να διασφαλιστεί η σωστή λειτουργία, πρέπει να τηρούνται μερικοί κανόνες: -```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)) +- Γράψτε λέξεις-κλειδιά, ονόματα συναρτήσεων, διαδικασίες, κ.λπ., σε **uppercase**. +- Γράψτε τα ονόματα στηλών και πινάκων με **μικρά γράμματα**. +- Πάντα να περνάτε συμβολοσειρές χρησιμοποιώντας **παραμέτρους**. + +```php +where('name = ' . $name); // **DISASTER**: ευάλωτο σε έγχυση SQL +where('name LIKE "%search%"'); // **WRONG**: περιπλέκει την αυτόματη αναγραφή εισαγωγικών +where('name LIKE ?', '%search%'); // **ΣΩΣΤΟ**: τιμή που περνάει ως παράμετρος + +where('name like ?', $name); // **WRONG**: παράγει: `name` `like` ? +where('name LIKE ?', $name); // **ΣΩΣΤΟ**: παράγει: `name` LIKE ? +where('LOWER(name) = ?', $value);// **ΣΩΣΤΟ**: LOWER(`όνομα`) = ? ``` -Επιλογές .[#toc-selections] -=========================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Δείτε τις δυνατότητες φιλτραρίσματος και περιορισμού των γραμμών [api:Nette\Database\Table\Selection]: +Φιλτράρει τα αποτελέσματα χρησιμοποιώντας συνθήκες WHERE. Η δύναμή του έγκειται στον έξυπνο χειρισμό διαφόρων τύπων τιμών και στην αυτόματη επιλογή τελεστών SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Ορισμός WHERE με χρήση του AND ως συγκολλητικού στοιχείου εάν παρέχονται δύο ή περισσότερες συνθήκες -| `$table->whereOr($where)` | Ορισμός WHERE με χρήση OR ως συγκολλητικό στοιχείο εάν παρέχονται δύο ή περισσότερες συνθήκες -| `$table->order($columns)` | Ορισμός ORDER BY, μπορεί να είναι έκφραση `('column DESC, id DESC')` -| `$table->select($columns)` | Ορισμός ανακτημένων στηλών, μπορεί να είναι έκφραση `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Ορισμός LIMIT και OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Ενεργοποιεί τη σελιδοποίηση -| `$table->group($columns)` | Ορισμός GROUP BY -| `$table->having($having)` | Ορισμός HAVING +Βασική χρήση: -Μπορούμε να χρησιμοποιήσουμε μια λεγόμενη [ρευστή διεπαφή |nette:introduction-to-object-oriented-programming#fluent-interfaces], για παράδειγμα `$table->where(...)->order(...)->limit(...)`. Πολλαπλές συνθήκες `where` ή `whereOr` συνδέονται με τον τελεστή `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' +``` +Χάρη στην αυτόματη ανίχνευση των κατάλληλων χειριστών, δεν χρειάζεται να χειρίζεστε ειδικές περιπτώσεις - η Nette τις χειρίζεται για εσάς: -where() .[#toc-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) +// Το σύμβολο θέσης ? μπορεί να χρησιμοποιηθεί χωρίς τελεστή: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Ο Nette Database Explorer μπορεί να προσθέσει αυτόματα τους απαραίτητους τελεστές για τις περασμένες τιμές: +Η μέθοδος χειρίζεται επίσης σωστά τις αρνητικές συνθήκες και τους άδειους πίνακες: -.[language-php] -| `$table->where('field', $value)` | field = $value -| `$table->where('field', null)` | field IS NULL -| `$table->where('field > ?', $val)` | πεδίο > $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 -- δεν βρίσκει τίποτα +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- βρίσκει τα πάντα +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- βρίσκει τα πάντα +// $table->where('NOT id ?', $ids); // ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτή η σύνταξη δεν υποστηρίζεται +``` -Μπορείτε να δώσετε placeholder ακόμη και χωρίς τον τελεστή στήλης. Αυτές οι κλήσεις είναι οι ίδιες. +Μπορείτε επίσης να περάσετε το αποτέλεσμα ενός άλλου ερωτήματος πίνακα ως παράμετρο, δημιουργώντας ένα υποερώτημα: ```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')); ``` -Αυτή η λειτουργία επιτρέπει τη δημιουργία του σωστού τελεστή με βάση την τιμή: +Οι συνθήκες μπορούν επίσης να περάσουν ως πίνακας, με τα στοιχεία να συνδυάζονται με τη χρήση 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', +]); ``` -Λειτουργεί και για άδειους πίνακες: +Στον πίνακα μπορούν να χρησιμοποιηθούν ζεύγη κλειδιών-τιμών και η Nette θα επιλέξει και πάλι αυτόματα τους σωστούς τελεστές: ```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], +]); +``` -// αυτό θα προκαλέσει μια εξαίρεση, αυτή η σύνταξη δεν υποστηρίζεται -$table->where('NOT id ?', $ids); +Μπορούμε επίσης να αναμειγνύουμε εκφράσεις SQL με placeholders και πολλαπλές παραμέτρους. Αυτό είναι χρήσιμο για σύνθετες συνθήκες με επακριβώς καθορισμένους τελεστές: + +```php +// WHERE (`ηλικία` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // Δύο παράμετροι περνούν ως πίνακας +]); ``` +Πολλαπλές κλήσεις στο `where()` συνδυάζουν αυτόματα τις συνθήκες με τη χρήση AND. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Παράδειγμα χρήσης χωρίς παραμέτρους: +Παρόμοιο με το `where()`, αλλά συνδυάζει τις συνθήκες χρησιμοποιώντας 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, ]); ``` -Χρησιμοποιούμε τις παραμέτρους. Εάν δεν καθορίσετε έναν τελεστή, ο Nette Database Explorer θα προσθέσει αυτόματα τον κατάλληλο: +Μπορούν επίσης να χρησιμοποιηθούν πιο σύνθετες εκφράσεις: ```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, ]); ``` -Το κλειδί μπορεί να περιέχει μια έκφραση που περιέχει ερωτηματικά μπαλαντέρ και στη συνέχεια να περάσετε παραμέτρους στην τιμή: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Προσθέτει μια συνθήκη για το πρωτεύον κλειδί του πίνακα: ```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]); +``` + +(π.χ. `foo_id`, `bar_id`), το περνάμε ως πίνακα: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Παραδείγματα χρήσης: +Καθορίζει τη σειρά με την οποία επιστρέφονται οι γραμμές. Μπορείτε να ταξινομήσετε με βάση μία ή περισσότερες στήλες, με αύξουσα ή φθίνουσα σειρά ή με βάση μια προσαρμοσμένη έκφραση: ```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 `δημιουργήθηκε` +$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() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Παραδείγματα χρήσης: +Καθορίζει τις στήλες που θα επιστραφούν από τη βάση δεδομένων. Από προεπιλογή, η Nette Database Explorer επιστρέφει μόνο τις στήλες που χρησιμοποιούνται πραγματικά στον κώδικα. Χρησιμοποιήστε τη μέθοδο `select()` όταν πρέπει να ανακτήσετε συγκεκριμένες εκφράσεις: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Τα ψευδώνυμα που ορίζονται με τη χρήση του `AS` είναι στη συνέχεια προσβάσιμα ως ιδιότητες του αντικειμένου `ActiveRow`: -limit() .[#toc-limit] ---------------------- +```php +foreach ($table as $row) { + echo $row->formatted_date; // πρόσβαση στο ψευδώνυμο +} +``` + + +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Παραδείγματα χρήσης: +Περιορίζει τον αριθμό των επιστρεφόμενων γραμμών (LIMIT) και προαιρετικά ορίζει μια μετατόπιση: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (επιστρέφει τις πρώτες 10 γραμμές) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Για σελιδοποίηση, είναι καταλληλότερο να χρησιμοποιείτε τη μέθοδο `page()`. -page() .[#toc-page] -------------------- -Ένας εναλλακτικός τρόπος για τον καθορισμό του ορίου και της μετατόπισης: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Απλοποιεί την σελιδοποίηση των αποτελεσμάτων. Δέχεται τον αριθμό σελίδας (ξεκινώντας από το 1) και τον αριθμό των στοιχείων ανά σελίδα. Προαιρετικά, μπορείτε να περάσετε μια αναφορά σε μια μεταβλητή όπου θα αποθηκευτεί ο συνολικός αριθμός σελίδων: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` - `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Ομαδοποιεί τις γραμμές με βάση τις καθορισμένες στήλες (GROUP BY). Χρησιμοποιείται συνήθως σε συνδυασμό με συναρτήσεις συνάθροισης: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Μετρά τον αριθμό των προϊόντων σε κάθε κατηγορία +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Παραδείγματα χρήσης: +Ορίζει μια συνθήκη για το φιλτράρισμα ομαδοποιημένων γραμμών (HAVING). Μπορεί να χρησιμοποιηθεί σε συνδυασμό με τη μέθοδο `group()` και τις συναρτήσεις συνάθροισης: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Βρίσκει κατηγορίες με περισσότερα από 100 προϊόντα +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Ανάγνωση δεδομένων +================== + +Για την ανάγνωση δεδομένων από τη βάση δεδομένων, υπάρχουν αρκετές χρήσιμες μέθοδοι: + +.[language-php] +| `foreach ($table as $key => $row)` | Επαναλαμβάνει όλες τις γραμμές, `$key` είναι η τιμή του πρωτεύοντος κλειδιού, `$row` είναι ένα αντικείμενο ActiveRow | +| `$row = $table->get($key)` | Επιστρέφει μία μόνο γραμμή με βάση το πρωτεύον κλειδί | +| `$row = $table->fetch()` | Επιστρέφει την τρέχουσα γραμμή και προωθεί τον δείκτη στην επόμενη | +| `$array = $table->fetchPairs()` | Δημιουργεί έναν συσχετιστικό πίνακα από τα αποτελέσματα | +| `$array = $table->fetchAll()` | Επιστρέφει όλες τις γραμμές ως πίνακα | +| `count($table)` | Επιστρέφει τον αριθμό των γραμμών στο αντικείμενο Selection | + +Το αντικείμενο [ActiveRow |api:Nette\Database\Table\ActiveRow] είναι μόνο για ανάγνωση. Αυτό σημαίνει ότι δεν μπορείτε να αλλάξετε τις τιμές των ιδιοτήτων του. Αυτός ο περιορισμός διασφαλίζει τη συνοχή των δεδομένων και αποτρέπει απροσδόκητες παρενέργειες. Τα δεδομένα αντλούνται από τη βάση δεδομένων και οποιεσδήποτε αλλαγές θα πρέπει να γίνονται ρητά και με ελεγχόμενο τρόπο. + + +`foreach` - Επανάληψη όλων των σειρών +------------------------------------- -Παραδείγματα χρήσης: +Ο ευκολότερος τρόπος εκτέλεσης ενός ερωτήματος και ανάκτησης γραμμών είναι η επανάληψη με τον βρόχο `foreach`. Εκτελεί αυτόματα το ερώτημα SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = πρωτεύον κλειδί, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Φιλτράρισμα με βάση άλλη τιμή πίνακα .[#toc-joining-key] --------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Εκτελεί ένα ερώτημα SQL και επιστρέφει μια γραμμή με βάση το πρωτεύον κλειδί της ή το `null` εάν δεν υπάρχει. + +```php +$book = $explorer->table('book')->get(123); // επιστρέφει ActiveRow με ID 123 ή null +if ($book) { + echo $book->title; +} +``` + -Αρκετά συχνά χρειάζεται να φιλτράρετε τα αποτελέσματα με βάση κάποια συνθήκη που αφορά έναν άλλο πίνακα της βάσης δεδομένων. Αυτού του είδους οι συνθήκες απαιτούν σύνδεση πινάκων. Ωστόσο, δεν χρειάζεται πλέον να τα γράφετε. +fetch(): ?ActiveRow .[method] +----------------------------- -Ας υποθέσουμε ότι πρέπει να πάρετε όλα τα βιβλία των οποίων το όνομα του συγγραφέα είναι 'Jon'. Το μόνο που χρειάζεται να γράψετε είναι το κλειδί σύνδεσης της σχέσης και το όνομα της στήλης στον συνδεδεμένο πίνακα. Το κλειδί σύνδεσης προκύπτει από τη στήλη που αναφέρεται στον πίνακα που θέλετε να συνδέσετε. Στο παράδειγμά μας (βλ. το σχήμα db) είναι η στήλη `author_id`, και αρκεί να χρησιμοποιήσετε μόνο το πρώτο μέρος της - `author` (η κατάληξη `_id` μπορεί να παραλειφθεί). `name` είναι μια στήλη του πίνακα `author` που θέλουμε να χρησιμοποιήσουμε. Μια συνθήκη για τον μεταφραστή βιβλίων (η οποία συνδέεται με τη στήλη `translator_id` ) μπορεί να δημιουργηθεί εξίσου εύκολα. +Επιστρέφει μια γραμμή και προωθεί τον εσωτερικό δείκτη στην επόμενη. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `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); +} ``` -Η λογική του κλειδιού σύνδεσης καθοδηγείται από την εφαρμογή των [συμβάσεων |api:Nette\Database\Conventions]. Σας ενθαρρύνουμε να χρησιμοποιήσετε το [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], το οποίο αναλύει τα ξένα κλειδιά σας και σας επιτρέπει να εργάζεστε εύκολα με αυτές τις σχέσεις. -Η σχέση μεταξύ του βιβλίου και του συγγραφέα του είναι 1:N. Η αντίστροφη σχέση είναι επίσης δυνατή. Την ονομάζουμε **backjoin**. Ρίξτε μια ματιά σε ένα άλλο παράδειγμα. Θα θέλαμε να αντλήσουμε όλους τους συγγραφείς, οι οποίοι έχουν γράψει περισσότερα από 3 βιβλία. Για να κάνουμε την αντίστροφη σύνδεση χρησιμοποιούμε τη δήλωση `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY`, επίσης η συνθήκη πρέπει να γραφτεί με τη μορφή της δήλωσης `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Επιστρέφει τα αποτελέσματα ως συσχετιστικό πίνακα. Το πρώτο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως κλειδί στον πίνακα και το δεύτερο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως τιμή: ```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', ...] ``` -Ίσως έχετε παρατηρήσει ότι η έκφραση σύνδεσης αναφέρεται στο βιβλίο, αλλά δεν είναι σαφές, αν κάνουμε σύνδεση μέσω του `author_id` ή του `translator_id`. Στο παραπάνω παράδειγμα, η Selection κάνει σύνδεση μέσω της στήλης `author_id` επειδή βρέθηκε μια αντιστοιχία με τον πίνακα προέλευσης - τον πίνακα `author`. Αν δεν υπήρχε τέτοια αντιστοιχία και υπήρχαν περισσότερες δυνατότητες, η Nette θα έριχνε [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Εάν καθορίζεται μόνο η στήλη κλειδί, η τιμή θα είναι ολόκληρη η γραμμή, δηλαδή το αντικείμενο `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` -Για να πραγματοποιήσετε μια σύνδεση μέσω της στήλης `translator_id`, δώστε μια προαιρετική παράμετρο εντός της έκφρασης σύνδεσης. +Εάν ως κλειδί ορίζεται το `null`, ο πίνακας θα είναι αριθμητικά δεικτοδοτημένος ξεκινώντας από το μηδέν: ```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', ...] +``` + +Μπορείτε επίσης να περάσετε ως παράμετρο ένα callback, το οποίο θα επιστρέψει είτε την ίδια την τιμή είτε ένα ζεύγος κλειδιού-τιμής για κάθε γραμμή. Εάν η επανάκληση επιστρέφει μόνο μια τιμή, το κλειδί θα είναι το πρωτεύον κλειδί της γραμμής: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Πρώτο βιβλίο (Jan Novak)', ...] + +// Η επανάκληση μπορεί επίσης να επιστρέψει έναν πίνακα με ένα ζεύγος κλειδιών και τιμών: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['First Book' => 'Jan Novak', ...] ``` -Ας ρίξουμε μια ματιά σε μερικές πιο δύσκολες εκφράσεις σύνδεσης. -Θα θέλαμε να βρούμε όλους τους συγγραφείς που έχουν γράψει κάτι για την PHP. Όλα τα βιβλία έχουν ετικέτες, οπότε θα πρέπει να επιλέξουμε εκείνους τους συγγραφείς που έχουν γράψει κάποιο βιβλίο με την ετικέτα PHP. +fetchAll(): array .[method] +--------------------------- + +Επιστρέφει όλες τις γραμμές ως συσχετιστικό πίνακα αντικειμένων `ActiveRow`, όπου τα κλειδιά είναι οι τιμές του πρωτεύοντος κλειδιού. ```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, ...), ...] ``` -Συγκεντρωτικά ερωτήματα .[#toc-aggregate-queries] -------------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Λήψη αριθμού γραμμών -| `$table->count("DISTINCT $column")` | Λήψη αριθμού διαφορετικών τιμών -| `$table->min($column)` | Λήψη ελάχιστης τιμής -| `$table->max($column)` | Λήψη μέγιστης τιμής -| `$table->sum($column)` | Λήψη του αθροίσματος όλων των τιμών -| `$table->aggregation("GROUP_CONCAT($column)")` | Εκτέλεση οποιασδήποτε συνάρτησης συνάθροισης +Η μέθοδος `count()` χωρίς παραμέτρους επιστρέφει τον αριθμό των γραμμών στο αντικείμενο `Selection`: -.[caution] -Η μέθοδος `count()` χωρίς καθορισμένες παραμέτρους επιλέγει όλες τις εγγραφές και επιστρέφει το μέγεθος του πίνακα, κάτι που είναι πολύ αναποτελεσματικό. Για παράδειγμα, αν πρέπει να υπολογίσετε τον αριθμό των γραμμών για τη σελιδοποίηση, καθορίζετε πάντα το πρώτο όρισμα. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // εναλλακτική λύση +``` +Σημείωση: Η μέθοδος `count()` με μια παράμετρο εκτελεί τη λειτουργία συνάθροισης COUNT στη βάση δεδομένων, όπως περιγράφεται παρακάτω. -Διαφυγή & παραπομπή .[#toc-escaping-quoting] -============================================ -Ο Database Explorer είναι έξυπνος και αποφεύγει τις παραμέτρους και τα αναγνωριστικά εισαγωγικά για εσάς. Ωστόσο, πρέπει να τηρούνται αυτοί οι βασικοί κανόνες: +ActiveRow::toArray(): array .[method] +------------------------------------- -- λέξεις-κλειδιά, συναρτήσεις, διαδικασίες πρέπει να είναι κεφαλαία. -- οι στήλες και οι πίνακες πρέπει να είναι πεζά -- να περνάτε μεταβλητές ως παραμέτρους, να μην τις συνυπολογίζετε +Μετατρέπει το αντικείμενο `ActiveRow` σε συσχετιστικό πίνακα όπου τα κλειδιά είναι ονόματα στηλών και οι τιμές είναι τα αντίστοιχα δεδομένα. ```php -->where('name like ?', 'John'); // ΛΑΘΟΣ! γενιές: `name` `like` ? -->where('name LIKE ?', 'John'); // ΣΩΣΤΟ +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// Το $bookArray θα είναι ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + -->where('KEY = ?', $value); // ΛΑΘΟΣ! Το KEY είναι λέξη κλειδί -->where('key = ?', $value); // ΣΩΣΤΟ: `key` = ? +Συγκέντρωση .[#toc-aggregation] +=============================== -->where('name = ' . $name); // ΛΑΘΟΣ! sql injection! -->where('name = ?', $name); // ΣΩΣΤΟ +Η κλάση `Selection` παρέχει μεθόδους για την εύκολη εκτέλεση συναρτήσεων άθροισης (COUNT, SUM, MIN, MAX, AVG, κ.λπ.). -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // Περνάτε μεταβλητές ως παραμέτρους, μην κάνετε συνένωση -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // ΣΩΣΤΟ +.[language-php] +| `count($expr)` | Μετράει τον αριθμό των γραμμών | +| `min($expr)` | Επιστρέφει την ελάχιστη τιμή σε μια στήλη | +| `max($expr)` | Επιστρέφει τη μέγιστη τιμή σε μια στήλη | +| `sum($expr)` | Επιστρέφει το άθροισμα των τιμών σε μια στήλη | +| `aggregation($function)` | Επιτρέπει οποιαδήποτε συνάρτηση άθροισης, όπως `AVG()` ή `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Εκτελεί ένα ερώτημα SQL με τη συνάρτηση COUNT και επιστρέφει το αποτέλεσμα. Αυτή η μέθοδος χρησιμοποιείται για να προσδιοριστεί πόσες γραμμές αντιστοιχούν σε μια συγκεκριμένη συνθήκη: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Σημείωση: η [count() |#count()] χωρίς παράμετρο επιστρέφει απλώς τον αριθμό των γραμμών στο αντικείμενο `Selection`. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Οι μέθοδοι `min()` και `max()` επιστρέφουν την ελάχιστη και τη μέγιστη τιμή στην καθορισμένη στήλη ή έκφραση: + +```php +// SELECT MAX(`τιμή`) FROM `προϊόντα` WHERE `ενεργό` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Επιστρέφει το άθροισμα των τιμών στην καθορισμένη στήλη ή έκφραση: + +```php +// SELECT SUM(`Τιμή` * `Είδη_σε_απόθεμα`) FROM `Προϊόντα` WHERE `ενεργό` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -Η λανθασμένη χρήση μπορεί να δημιουργήσει κενά ασφαλείας +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Επιτρέπει την εκτέλεση οποιασδήποτε συνάρτησης άθροισης. -Λήψη δεδομένων .[#toc-fetching-data] -==================================== +```php +// Υπολογίζει τη μέση τιμή των προϊόντων μιας κατηγορίας +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -| `foreach ($table as $id => $row)` | Επανάληψη σε όλες τις γραμμές του αποτελέσματος -| `$row = $table->get($id)` | Λήψη μεμονωμένης γραμμής με αναγνωριστικό $id από τον πίνακα -| `$row = $table->fetch()` | Λήψη της επόμενης γραμμής από το αποτέλεσμα -| `$array = $table->fetchPairs($key, $value)` | Λήψη όλων των τιμών σε συσχετιστικό πίνακα -| `$array = $table->fetchPairs($value)` | Λήψη όλων των γραμμών σε συσχετιστικό πίνακα -| `count($table)` | Λήψη αριθμού γραμμών στο σύνολο αποτελεσμάτων +// Συνδυάζει τις ετικέτες προϊόντων σε μια ενιαία συμβολοσειρά +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Εάν χρειάζεται να αθροίσουμε αποτελέσματα που τα ίδια προκύπτουν από μια συνάθροιση και ομαδοποίηση (π.χ. `SUM(value)` πάνω σε ομαδοποιημένες γραμμές), καθορίζουμε τη συνάρτηση συνάθροισης που θα εφαρμοστεί σε αυτά τα ενδιάμεσα αποτελέσματα ως δεύτερο όρισμα: + +```php +// Υπολογίζει τη συνολική τιμή των προϊόντων σε απόθεμα για κάθε κατηγορία και στη συνέχεια αθροίζει τις τιμές αυτές. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +Σε αυτό το παράδειγμα, υπολογίζουμε πρώτα τη συνολική τιμή των προϊόντων σε κάθε κατηγορία (`SUM(price * stock) AS category_total`) και ομαδοποιούμε τα αποτελέσματα με βάση το `category_id`. Στη συνέχεια χρησιμοποιούμε το `aggregation('SUM(category_total)', 'SUM')` για να αθροίσουμε αυτά τα επιμέρους αθροίσματα. Το δεύτερο όρισμα `'SUM'` καθορίζει τη συνάρτηση άθροισης που θα εφαρμοστεί στα ενδιάμεσα αποτελέσματα. Εισαγωγή, ενημέρωση και διαγραφή .[#toc-insert-update-delete] ============================================================= -Η μέθοδος `insert()` δέχεται πίνακα αντικειμένων Traversable (για παράδειγμα [ArrayHash |utils:arrays#ArrayHash] που επιστρέφει [μορφές |forms:]): +Η Nette Database Explorer απλοποιεί την εισαγωγή, την ενημέρωση και τη διαγραφή δεδομένων. Όλες οι αναφερόμενες μέθοδοι πετούν ένα `Nette\Database\DriverException` σε περίπτωση σφάλματος. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Εισάγει νέες εγγραφές σε έναν πίνακα. + +**Εισαγωγή μίας μόνο εγγραφής:** + +Η νέα εγγραφή παραδίδεται ως συσχετιστικός πίνακας ή αντικείμενο επανάληψης (όπως το `ArrayHash` που χρησιμοποιείται στις [φόρμες |forms:]), όπου τα κλειδιά αντιστοιχούν στα ονόματα των στηλών του πίνακα. + +Εάν ο πίνακας έχει καθορισμένο πρωτεύον κλειδί, η μέθοδος επιστρέφει ένα αντικείμενο `ActiveRow`, το οποίο φορτώνεται εκ νέου από τη βάση δεδομένων για να αντικατοπτρίζει τυχόν αλλαγές που έγιναν σε επίπεδο βάσης δεδομένων (π.χ. εναύσματα, προεπιλεγμένες τιμές στηλών ή υπολογισμούς αυτόματης αύξησης). Αυτό διασφαλίζει τη συνέπεια των δεδομένων και το αντικείμενο περιέχει πάντα τα τρέχοντα δεδομένα της βάσης δεδομένων. Εάν δεν έχει οριστεί ρητά ένα πρωτεύον κλειδί, η μέθοδος επιστρέφει τα δεδομένα εισόδου ως πίνακα. ```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 είναι μια περίπτωση της ActiveRow που περιέχει τα πλήρη δεδομένα της εισαγόμενης γραμμής, +// συμπεριλαμβανομένου του αναγνωριστικού που δημιουργείται αυτόματα και οποιωνδήποτε αλλαγών που πραγματοποιούνται από triggers +echo $row->id; // Εξάγει το ID του νεοεισαχθέντος χρήστη +echo $row->created_at; // Εκδίδει την ώρα δημιουργίας, εάν έχει οριστεί από ένα σκανδάλη ``` -Εάν έχει οριστεί πρωτεύον κλειδί στον πίνακα, επιστρέφεται ένα αντικείμενο ActiveRow που περιέχει την εισαγόμενη γραμμή. +**Εισαγωγή πολλαπλών εγγραφών ταυτόχρονα:** -Πολλαπλή εισαγωγή: +Η μέθοδος `insert()` σας επιτρέπει να εισάγετε πολλαπλές εγγραφές με ένα μόνο ερώτημα SQL. Σε αυτή την περίπτωση, επιστρέφει τον αριθμό των εισαγόμενων γραμμών. ```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 θα είναι 2 +``` + +Μπορείτε επίσης να περάσετε ως παράμετρο ένα αντικείμενο `Selection` με μια επιλογή δεδομένων. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -DateTime μπορούν να περάσουν ως παράμετροι: +**Εισαγωγή ειδικών τιμών:** + +Οι τιμές μπορούν να περιλαμβάνουν αρχεία, αντικείμενα `DateTime` ή λογοτεχνικά στοιχεία SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // ή $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // εισάγει το αρχείο + 'name' => 'John', + 'created_at' => new DateTime, // μετατρέπει σε μορφή βάσης δεδομένων + 'avatar' => fopen('image.jpg', 'rb'), // εισάγει περιεχόμενο δυαδικού αρχείου + 'uuid' => $explorer::literal('UUID()'), // καλεί τη συνάρτηση UUID() ]); ``` -Ενημέρωση (επιστρέφει τον αριθμό των επηρεαζόμενων γραμμών): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Ενημερώνει γραμμές σε έναν πίνακα με βάση ένα καθορισμένο φίλτρο. Επιστρέφει τον αριθμό των σειρών που τροποποιήθηκαν πραγματικά. + +Οι προς ενημέρωση στήλες περνούν ως συσχετιστικός πίνακας ή αντικείμενο επανάληψης (όπως το `ArrayHash` που χρησιμοποιείται στις [φόρμες |forms:]), όπου τα κλειδιά αντιστοιχούν στα ονόματα των στηλών του πίνακα: ```php -$count = $explorer->table('users') - ->where('id', 10) // πρέπει να καλείται πριν από την 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 ``` -Για την ενημέρωση μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` a `-=`: +Για να αλλάξετε αριθμητικές τιμές, μπορείτε να χρησιμοποιήσετε τους τελεστές `+=` και `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // αυξάνει την τιμή της στήλης "πόντοι" κατά 1 + 'coins-=' => 1, // μειώνει την τιμή της στήλης "νομίσματα" κατά 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Διαγραφή (επιστρέφει τον αριθμό των διαγραμμένων γραμμών): + +Selection::delete(): int .[method] +---------------------------------- + +Διαγράφει γραμμές από έναν πίνακα με βάση ένα καθορισμένο φίλτρο. Επιστρέφει τον αριθμό των διαγραμμένων γραμμών. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Κατά την κλήση των `update()` ή `delete()`, φροντίστε να χρησιμοποιήσετε το `where()` για να καθορίσετε τις γραμμές που θα ενημερωθούν ή θα διαγραφούν. Εάν δεν χρησιμοποιηθεί το `where()`, η λειτουργία θα εκτελεστεί σε ολόκληρο τον πίνακα! + -Εργασία με σχέσεις .[#toc-working-with-relationships] -===================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Ενημερώνει τα δεδομένα σε μια γραμμή της βάσης δεδομένων που αντιπροσωπεύεται από το αντικείμενο `ActiveRow`. Δέχεται επαναλήψιμα δεδομένα ως παράμετρο, όπου τα κλειδιά είναι ονόματα στηλών. Για να αλλάξετε αριθμητικές τιμές, μπορείτε να χρησιμοποιήσετε τους τελεστές `+=` και `-=`: -Έχει μία σχέση .[#toc-has-one-relation] ---------------------------------------- -Το Has one relation είναι μια συνηθισμένη περίπτωση χρήσης. Το βιβλίο *έχει έναν* συγγραφέα. Το βιβλίο *έχει έναν* μεταφραστή. Η λήψη σχετικής σειράς γίνεται κυρίως με τη μέθοδο `ref()`. Δέχεται δύο ορίσματα: το όνομα του πίνακα-στόχου και τη στήλη σύνδεσης της πηγής. Βλέπε παράδειγμα: +Αφού εκτελεστεί η ενημέρωση, το `ActiveRow` φορτώνεται αυτόματα εκ νέου από τη βάση δεδομένων για να αντικατοπτρίζει τυχόν αλλαγές που έγιναν σε επίπεδο βάσης δεδομένων (π.χ. εναύσματα). Η μέθοδος επιστρέφει το `true` μόνο εάν έχει πραγματοποιηθεί πραγματική αλλαγή δεδομένων. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // αυξάνει τον αριθμό προβολών +]); +echo $article->views; // Εκδίδει τον τρέχοντα αριθμό προβολών ``` - `author` Το πρωτεύον κλειδί του συγγραφέα αναζητείται από τη στήλη `book.author_id`. Η μέθοδος Ref() επιστρέφει την περίπτωση ActiveRow ή null αν δεν υπάρχει κατάλληλη εγγραφή. Η γραμμή που επιστρέφεται είναι μια περίπτωση της ActiveRow, οπότε μπορούμε να εργαστούμε με αυτήν με τον ίδιο τρόπο όπως με την εγγραφή του βιβλίου. +Αυτή η μέθοδος ενημερώνει μόνο μια συγκεκριμένη γραμμή στη βάση δεδομένων. Για μαζικές ενημερώσεις πολλών γραμμών, χρησιμοποιήστε τη μέθοδο [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Διαγράφει μια γραμμή από τη βάση δεδομένων που αντιπροσωπεύεται από το αντικείμενο `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Διαγράφει το βιβλίο με ID 1 +``` -// ή απευθείας -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Αυτή η μέθοδος διαγράφει μόνο μια συγκεκριμένη γραμμή στη βάση δεδομένων. Για μαζική διαγραφή πολλών γραμμών, χρησιμοποιήστε τη μέθοδο [Selection::delete() |#Selection::delete()]. + + +Σχέσεις μεταξύ πινάκων .[#toc-relationships-between-tables] +=========================================================== + +Στις σχεσιακές βάσεις δεδομένων, τα δεδομένα χωρίζονται σε πολλούς πίνακες και συνδέονται μέσω ξένων κλειδιών. Ο Nette Database Explorer προσφέρει έναν επαναστατικό τρόπο εργασίας με αυτές τις σχέσεις - χωρίς να γράφει ερωτήματα JOIN ή να απαιτεί οποιαδήποτε διαμόρφωση ή δημιουργία οντοτήτων. + +Για την επίδειξη, θα χρησιμοποιήσουμε τη βάση δεδομένων **παράδειγμα**[(διαθέσιμη στο GitHub |https://github.com/nette-examples/books]). Η βάση δεδομένων περιλαμβάνει τους ακόλουθους πίνακες: + +- `author` - συγγραφείς και μεταφραστές (στήλες `id`, `name`, `web`, `born`) +- `book` - βιβλία (στήλες `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - ετικέτες (στήλες `id`, `name`) +- `book_tag` - πίνακας συνδέσμων μεταξύ βιβλίων και ετικετών (στήλες `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Δομή της βάσης δεδομένων .<> + +Σε αυτό το παράδειγμα βάσης δεδομένων βιβλίων, βρίσκουμε διάφορους τύπους σχέσεων (απλοποιημένους σε σχέση με την πραγματικότητα): + +- **Ένα προς πολλά (1:N)** - Κάθε βιβλίο **έχει έναν** συγγραφέα- ένας συγγραφέας μπορεί να γράψει **πολλαπλά** βιβλία. +- **Νέο προς πολλά (0:N)** - Ένα βιβλίο **μπορεί να έχει** έναν μεταφραστή- ένας μεταφραστής μπορεί να μεταφράσει **πολλαπλά** βιβλία. +- **Μηδέν προς ένα (0:1)** - Ένα βιβλίο **μπορεί να έχει** συνέχεια. +- **Πολλοί-προς-πολλούς (Μ:Ν)** - Ένα βιβλίο **μπορεί να έχει πολλές** ετικέτες και μια ετικέτα μπορεί να ανατεθεί σε **πολλά** βιβλία. + +Σε αυτές τις σχέσεις, υπάρχει πάντα ένας πίνακας **γονέας** και ένας πίνακας **παιδί**. Για παράδειγμα, στη σχέση μεταξύ συγγραφέων και βιβλίων, ο πίνακας `author` είναι ο γονέας και ο πίνακας `book` είναι το παιδί - μπορείτε να το φανταστείτε ότι ένα βιβλίο "ανήκει" πάντα σε έναν συγγραφέα. Αυτό αντικατοπτρίζεται και στη δομή της βάσης δεδομένων: ο πίνακας-παιδί `book` περιέχει το ξένο κλειδί `author_id`, το οποίο παραπέμπει στον πίνακα-γονέα `author`. + +Αν θέλουμε να εμφανίσουμε τα βιβλία μαζί με τα ονόματα των συγγραφέων τους, έχουμε δύο επιλογές. Είτε ανακτούμε τα δεδομένα χρησιμοποιώντας ένα ενιαίο ερώτημα SQL με ένα JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Ή ανακτούμε τα δεδομένα σε δύο βήματα - πρώτα τα βιβλία, μετά τους συγγραφείς τους - και τα συναρμολογούμε σε PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Το βιβλίο έχει επίσης έναν μεταφραστή, οπότε το να πάρουμε το όνομα του μεταφραστή είναι αρκετά εύκολο. +Η δεύτερη προσέγγιση είναι, παραδόξως, **πιο αποδοτική**. Τα δεδομένα ανακτώνται μόνο μία φορά και μπορούν να αξιοποιηθούν καλύτερα στην κρυφή μνήμη. Έτσι ακριβώς λειτουργεί ο Nette Database Explorer - χειρίζεται τα πάντα κάτω από την κουκούλα και σας παρέχει ένα καθαρό 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 είναι μια εγγραφή από τον πίνακα 'author' + echo 'translated by: ' . $book->translator?->name; +} ``` -Όλα αυτά είναι μια χαρά, αλλά είναι κάπως δυσκίνητα, δεν νομίζετε; Η Εξερεύνηση βάσης δεδομένων περιέχει ήδη τους ορισμούς των ξένων κλειδιών, οπότε γιατί να μην τους χρησιμοποιήσετε αυτόματα; Ας το κάνουμε αυτό! -Αν καλέσουμε ιδιότητα, η οποία δεν υπάρχει, το ActiveRow προσπαθεί να επιλύσει το όνομα της καλούσας ιδιότητας ως σχέση 'έχει μία'. Η λήψη αυτής της ιδιότητας είναι το ίδιο με την κλήση της μεθόδου ref() με ένα μόνο όρισμα. Θα ονομάσουμε το μοναδικό όρισμα **key**. Το κλειδί θα επιλυθεί σε συγκεκριμένη σχέση ξένου κλειδιού. Το περασμένο κλειδί συγκρίνεται με τις στήλες της γραμμής, και αν ταιριάζει, το ξένο κλειδί που ορίζεται στην αντίστοιχη στήλη χρησιμοποιείται για τη λήψη δεδομένων από τον σχετικό πίνακα-στόχο. Βλέπε παράδειγμα: +Πρόσβαση στον γονικό πίνακα .[#toc-accessing-the-parent-table] +-------------------------------------------------------------- + +Η πρόσβαση στον γονικό πίνακα είναι απλή. Πρόκειται για σχέσεις όπως *ένα βιβλίο έχει έναν συγγραφέα* ή *ένα βιβλίο μπορεί να έχει έναν μεταφραστή*. Η σχετική εγγραφή μπορεί να προσπελαστεί μέσω της ιδιότητας του αντικειμένου `ActiveRow` - το όνομα της ιδιότητας ταιριάζει με το όνομα της στήλης του ξένου κλειδιού χωρίς την κατάληξη `id`: ```php -$book->author->name; -// το ίδιο με -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // βρίσκει τον συγγραφέα μέσω της στήλης 'author_id' +echo $book->translator?->name; // βρίσκει τον μεταφραστή μέσω της στήλης 'translator_id'. ``` -Η περίπτωση ActiveRow δεν έχει στήλη author. Όλες οι στήλες βιβλίων αναζητούνται για μια αντιστοιχία με το *key*. Η αντιστοίχιση σε αυτή την περίπτωση σημαίνει ότι το όνομα της στήλης πρέπει να περιέχει το κλειδί. Έτσι, στο παραπάνω παράδειγμα, η στήλη `author_id` περιέχει τη συμβολοσειρά 'author' και επομένως ταιριάζει με το κλειδί 'author'. Αν θέλετε να βρείτε τον μεταφραστή του βιβλίου, μπορείτε απλώς να χρησιμοποιήσετε π.χ. το 'translator' ως κλειδί, επειδή το κλειδί 'translator' θα ταιριάζει με τη στήλη `translator_id`. Μπορείτε να μάθετε περισσότερα για τη λογική αντιστοίχισης κλειδιών στο κεφάλαιο [Joining expressions |#joining-key]. +Κατά την πρόσβαση στην ιδιότητα `$book->author`, ο Explorer αναζητά μια στήλη στον πίνακα `book` που περιέχει τη συμβολοσειρά `author` (π.χ. `author_id`). Με βάση την τιμή σε αυτή τη στήλη, ανακτά την αντίστοιχη εγγραφή από τον πίνακα `author` και την επιστρέφει ως αντικείμενο `ActiveRow`. Ομοίως, το `$book->translator` χρησιμοποιεί τη στήλη `translator_id`. Δεδομένου ότι η στήλη `translator_id` μπορεί να περιέχει `null`, χρησιμοποιείται ο τελεστής `?->`. + +Μια εναλλακτική προσέγγιση παρέχεται από τη μέθοδο `ref()`, η οποία δέχεται δύο ορίσματα - το όνομα του πίνακα-στόχου και τη στήλη σύνδεσης - και επιστρέφει ένα αντικείμενο `ActiveRow` ή `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; // σύνδεση με τον συγγραφέα +echo $book->ref('author', 'translator_id')->name; // σύνδεσμος προς τον μεταφραστή ``` -Αν θέλετε να αντλήσετε πολλά βιβλία, θα πρέπει να χρησιμοποιήσετε την ίδια προσέγγιση. Ο Nette Database Explorer θα αντλήσει συγγραφείς και μεταφραστές για όλα τα βιβλία που θα αντλήσετε ταυτόχρονα. +Η μέθοδος `ref()` είναι χρήσιμη εάν δεν μπορεί να χρησιμοποιηθεί πρόσβαση με βάση την ιδιότητα, για παράδειγμα, όταν ο πίνακας περιέχει μια στήλη με το ίδιο όνομα με την ιδιότητα (`author`). Σε άλλες περιπτώσεις, συνιστάται η χρήση της πρόσβασης με βάση την ιδιότητα για καλύτερη αναγνωσιμότητα. + +Ο Explorer βελτιστοποιεί αυτόματα τα ερωτήματα στη βάση δεδομένων. Κατά την επανάληψη των βιβλίων και την πρόσβαση στις σχετικές εγγραφές τους (συγγραφείς, μεταφραστές), ο Explorer δεν δημιουργεί ένα ερώτημα για κάθε βιβλίο ξεχωριστά. Αντ' αυτού, εκτελεί μόνο **ένα ερώτημα SELECT για κάθε τύπο σχέσης**, μειώνοντας σημαντικά το φορτίο της βάσης δεδομένων. Για παράδειγμα: ```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; } ``` -Ο κώδικας θα εκτελέσει μόνο αυτά τα 3 ερωτήματα: +Αυτός ο κώδικας θα εκτελέσει μόνο τρία βελτιστοποιημένα ερωτήματα στη βάση δεδομένων: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Η λογική για τον προσδιορισμό της συνδετικής στήλης καθορίζεται από την υλοποίηση των [Συμβάσεων |api:Nette\Database\Conventions]. Συνιστούμε τη χρήση [του DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], το οποίο αναλύει τα ξένα κλειδιά και σας επιτρέπει να εργάζεστε απρόσκοπτα με τις υπάρχουσες σχέσεις πινάκων. -Has Many Relation .[#toc-has-many-relation] -------------------------------------------- -Η σχέση 'Έχει πολλούς' είναι απλώς η αντίστροφη σχέση 'έχει έναν'. Ο συγγραφέας *έχει* γράψει *πολλά* βιβλία. Ο συγγραφέας *έχει* μεταφράσει *πολλά* βιβλία. Όπως μπορείτε να δείτε, αυτός ο τύπος σχέσης είναι λίγο πιο δύσκολος επειδή η σχέση είναι 'ονομαστική' ('έγραψε', 'μετέφρασε'). Η περίπτωση ActiveRow έχει τη μέθοδο `related()`, η οποία θα επιστρέψει πίνακα σχετικών καταχωρήσεων. Οι εγγραφές είναι επίσης περιπτώσεις ActiveRow. Δείτε το παράδειγμα παρακάτω: +Πρόσβαση στον πίνακα "παιδί .[#toc-accessing-the-child-table] +------------------------------------------------------------- + +Η πρόσβαση στον πίνακα "παιδί" λειτουργεί προς την αντίθετη κατεύθυνση. Τώρα ρωτάμε *ποια βιβλία έγραψε αυτός ο συγγραφέας* ή *ποια βιβλία μετέφρασε αυτός ο μεταφραστής*. Για αυτού του είδους το ερώτημα, χρησιμοποιούμε τη μέθοδο `related()`, η οποία επιστρέφει ένα αντικείμενο `Selection` με σχετικές εγγραφές. Ακολουθεί ένα παράδειγμα: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Βγάζει όλα τα βιβλία που έχει γράψει ο συγγραφέας foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Εκδίδει όλα τα βιβλία που έχουν μεταφραστεί από τον συγγραφέα foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Η μέθοδος `related()` δέχεται την πλήρη περιγραφή join που περνάει ως δύο ορίσματα ή ως ένα όρισμα ενωμένο με τελεία. Το πρώτο όρισμα είναι ο πίνακας-στόχος, το δεύτερο είναι η στήλη-στόχος. +Η μέθοδος `related()` δέχεται την περιγραφή της σχέσης ως ενιαίο όρισμα με τη χρήση σημείωσης τελείας ή ως δύο ξεχωριστά ορίσματα: ```php -$author->related('book.translator_id'); -// το ίδιο με -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // ένα μόνο επιχείρημα +$author->related('book', 'translator_id'); // δύο επιχειρήματα ``` -Μπορείτε να χρησιμοποιήσετε τις ευρετικές λειτουργίες του Nette Database Explorer που βασίζονται σε ξένα κλειδιά και να δώσετε μόνο το όρισμα **key**. Το κλειδί θα συγκριθεί με όλα τα ξένα κλειδιά που δείχνουν στον τρέχοντα πίνακα (`author` table). Εάν υπάρχει ταύτιση, ο Nette Database Explorer θα χρησιμοποιήσει αυτό το ξένο κλειδί, διαφορετικά θα πετάξει [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] ή [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Μπορείτε να βρείτε περισσότερα για τη λογική αντιστοίχισης κλειδιών στο κεφάλαιο [Joining expressions |#joining-key]. +Ο Explorer μπορεί να εντοπίσει αυτόματα τη σωστή στήλη σύνδεσης με βάση το όνομα του γονικού πίνακα. Σε αυτή την περίπτωση, συνδέει μέσω της στήλης `book.author_id` επειδή το όνομα του πίνακα προέλευσης είναι `author`: -Φυσικά, μπορείτε να καλέσετε τις σχετικές μεθόδους για όλους τους συγγραφείς που ανακτήθηκαν, ο Nette Database Explorer θα ανακτήσει και πάλι τα κατάλληλα βιβλία ταυτόχρονα. +```php +$author->related('book'); // χρησιμοποιεί book.author_id +``` + +Εάν υπάρχουν πολλαπλές πιθανές συνδέσεις, ο Explorer θα πετάξει μια εξαίρεση [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Μπορούμε, φυσικά, να χρησιμοποιήσουμε τη μέθοδο `related()` και κατά την επανάληψη πολλαπλών εγγραφών σε έναν βρόχο, και ο Explorer θα βελτιστοποιήσει αυτόματα τα ερωτήματα και σε αυτή την περίπτωση: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Το παραπάνω παράδειγμα θα εκτελέσει μόνο δύο ερωτήματα: +Αυτός ο κώδικας παράγει μόνο δύο αποδοτικά ερωτήματα SQL: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Δημιουργία Explorer χειροκίνητα .[#toc-creating-explorer-manually] -================================================================== +Σχέση Πολλοί-προς-Πολλούς .[#toc-many-to-many-relationship] +----------------------------------------------------------- + +Για μια σχέση πολλών προς πολλούς (Μ:Ν), απαιτείται ένας **πίνακας σύνδεσης** (στην περίπτωσή μας, `book_tag`). Ο πίνακας αυτός περιέχει δύο στήλες ξένου κλειδιού (`book_id`, `tag_id`). Κάθε στήλη αναφέρεται στο πρωτεύον κλειδί ενός από τους συνδεδεμένους πίνακες. Για να ανακτήσουμε τα συνδεδεμένα δεδομένα, πρώτα αντλούμε εγγραφές από τον πίνακα σύνδεσης χρησιμοποιώντας το `related('book_tag')`, και στη συνέχεια συνεχίζουμε με τα δεδομένα-στόχο: + +```php +$book = $explorer->table('book')->get(1); +// Βγάζει τα ονόματα των ετικετών που έχουν αντιστοιχιστεί στο βιβλίο +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // αντλεί το όνομα της ετικέτας μέσω του πίνακα συνδέσμων +} + +$tag = $explorer->table('tag')->get(1); +// Αντίθετη κατεύθυνση: εξάγει τους τίτλους των βιβλίων με αυτή την ετικέτα +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // αντλεί τον τίτλο του βιβλίου +} +``` -Μια σύνδεση βάσης δεδομένων μπορεί να δημιουργηθεί χρησιμοποιώντας τη διαμόρφωση της εφαρμογής. Σε τέτοιες περιπτώσεις δημιουργείται μια υπηρεσία `Nette\Database\Explorer` και μπορεί να περάσει ως εξάρτηση χρησιμοποιώντας το DI container. +Ο Explorer βελτιστοποιεί και πάλι τα ερωτήματα SQL σε μια αποδοτική μορφή: -Ωστόσο, εάν ο Nette Database Explorer χρησιμοποιείται ως αυτόνομο εργαλείο, πρέπει να δημιουργηθεί χειροκίνητα μια περίπτωση του αντικειμένου `Nette\Database\Explorer`. +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Ερώτηση μέσω σχετικών πινάκων .[#toc-querying-through-related-tables] +--------------------------------------------------------------------- + +Στις μεθόδους `where()`, `select()`, `order()` και `group()`, μπορείτε να χρησιμοποιήσετε ειδικούς συμβολισμούς για να αποκτήσετε πρόσβαση σε στήλες από άλλους πίνακες. Ο Explorer δημιουργεί αυτόματα τις απαιτούμενες JOINs. + +Ο συμβολισμός **Dot** (`parent_table.column`) χρησιμοποιείται για σχέσεις 1:Ν, όπως φαίνεται από την οπτική γωνία του μητρικού πίνακα: ```php -// $storage implements Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Βρίσκει βιβλία των οποίων τα ονόματα των συγγραφέων αρχίζουν με 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Ταξινομεί τα βιβλία με βάση το όνομα του συγγραφέα κατά φθίνουσα σειρά +$books->order('author.name DESC'); + +// Βγάζει τον τίτλο του βιβλίου και το όνομα του συγγραφέα +$books->select('book.title, author.name'); +``` + +**Ο συμβολισμός με τελεία** χρησιμοποιείται για σχέσεις 1:Ν από την οπτική γωνία του γονικού πίνακα: + +```php +$authors = $explorer->table('author'); + +// Βρίσκει συγγραφείς που έγραψαν ένα βιβλίο με 'PHP' στον τίτλο +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Μετράει τον αριθμό των βιβλίων για κάθε συγγραφέα +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Στο παραπάνω παράδειγμα με συμβολισμό άνω και κάτω τελείας (`:book.title`), η στήλη ξένου κλειδιού δεν προσδιορίζεται ρητά. Η Εξερεύνηση εντοπίζει αυτόματα τη σωστή στήλη με βάση το όνομα του γονικού πίνακα. Σε αυτή την περίπτωση, συνδέεται μέσω της στήλης `book.author_id` επειδή το όνομα του πίνακα προέλευσης είναι `author`. Εάν υπάρχουν πολλαπλές πιθανές συνδέσεις, ο Explorer πετάει την εξαίρεση [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Η στήλη σύνδεσης μπορεί να καθοριστεί ρητά σε παρένθεση: + +```php +// Βρίσκει συγγραφείς που μετέφρασαν ένα βιβλίο με το 'PHP' στον τίτλο +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Οι συμβολισμοί μπορούν να συνδεθούν αλυσιδωτά για πρόσβαση σε δεδομένα σε πολλούς πίνακες: + +```php +// Βρίσκει συγγραφείς βιβλίων με ετικέτα 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Επέκταση των όρων για JOIN .[#toc-extending-conditions-for-join] +---------------------------------------------------------------- + +Η μέθοδος `joinWhere()` προσθέτει πρόσθετες συνθήκες στις συνδέσεις πινάκων στην SQL μετά τη λέξη-κλειδί `ON`. + +Για παράδειγμα, ας πούμε ότι θέλουμε να βρούμε βιβλία που έχουν μεταφραστεί από έναν συγκεκριμένο μεταφραστή: + +```php +// Βρίσκει βιβλία μεταφρασμένα από έναν μεταφραστή με το όνομα 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +Στη συνθήκη `joinWhere()`, μπορείτε να χρησιμοποιήσετε τις ίδιες δομές όπως στη μέθοδο `where()` - τελεστές, σημεία τοποθέτησης, πίνακες τιμών ή εκφράσεις SQL. + +Για πιο σύνθετα ερωτήματα με πολλαπλές JOINs, μπορούν να οριστούν ψευδώνυμα πινάκων: + +```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) +``` + +Σημειώστε ότι ενώ η μέθοδος `where()` προσθέτει συνθήκες στη ρήτρα `WHERE`, η μέθοδος `joinWhere()` επεκτείνει τις συνθήκες στη ρήτρα `ON` κατά τη διάρκεια των ενώσεων πινάκων. + + +Χειροκίνητη δημιουργία Explorer .[#toc-manually-creating-explorer] +================================================================== + +Εάν δεν χρησιμοποιείτε το δοχείο Nette DI, μπορείτε να δημιουργήσετε μια περίπτωση του `Nette\Database\Explorer` με μη αυτόματο τρόπο: + +```php +use Nette\Database; + +// $storage υλοποιεί Nette\Caching\Storage, π.χ.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// σύνδεση με βάση δεδομένων +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// διαχειρίζεται την αντανάκλαση της δομής της βάσης δεδομένων +$structure = new Database\Structure($connection, $storage); +// ορίζει κανόνες για την αντιστοίχιση ονομάτων πινάκων, στηλών και ξένων κλειδιών +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/el/security.texy b/database/el/security.texy new file mode 100644 index 0000000000..388c94b00a --- /dev/null +++ b/database/el/security.texy @@ -0,0 +1,145 @@ +Κίνδυνοι ασφαλείας +****************** + +.[perex] +Οι βάσεις δεδομένων συχνά περιέχουν ευαίσθητα δεδομένα και επιτρέπουν επικίνδυνες λειτουργίες. Η Nette Database παρέχει μια σειρά από χαρακτηριστικά ασφαλείας. Ωστόσο, είναι ζωτικής σημασίας να κατανοήσετε τη διαφορά μεταξύ ασφαλών και μη ασφαλών API. + + +Εγχείρηση SQL .[#toc-sql-injection] +=================================== + +Η έγχυση SQL είναι ο σοβαρότερος κίνδυνος ασφάλειας όταν εργάζεστε με βάσεις δεδομένων. Συμβαίνει όταν η μη ελεγχόμενη είσοδος του χρήστη γίνεται μέρος ενός ερωτήματος SQL. Ένας εισβολέας μπορεί να εισάγει τις δικές του εντολές SQL, αποκτώντας ή τροποποιώντας δεδομένα στη βάση δεδομένων. + +```php +// ❌ UNSAFE CODE - ευάλωτος σε έγχυση SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Ο επιτιθέμενος μπορεί να εισάγει κάτι σαν: ' OR '1'='1 +// Το ερώτημα που θα προκύψει θα είναι: +// OR '1'='1'. +// Αυτό επιστρέφει όλους τους χρήστες! +``` + +Το ίδιο ισχύει και για τον Database Explorer: + +```php +// ❌ UNSAFE CODE +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Ασφαλή παραμετροποιημένα ερωτήματα .[#toc-safe-parameterized-queries] +===================================================================== + +Ο ασφαλής τρόπος εισαγωγής τιμών σε ερωτήματα SQL είναι μέσω παραμετροποιημένων ερωτημάτων. Η Nette Database παρέχει διάφορους τρόπους για τη χρήση τους. + + +Ερωτηµατικά σηµάδια συµπλήρωσης .[#toc-placeholder-question-marks] +------------------------------------------------------------------ + +Η απλούστερη μέθοδος είναι η χρήση ερωτηματικών τύπου placeholder: + +```php +// ✅ Ασφαλή παραμετροποιημένα ερωτήματα +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Ασφαλής συνθήκη στην Εξερεύνηση +$table->where('name = ?', $_GET['name']); +``` + +Το ίδιο ισχύει και για όλες τις άλλες μεθόδους της Εξερεύνησης βάσης δεδομένων που επιτρέπουν την εισαγωγή εκφράσεων με ερωτηματικά και παραμέτρους με εικονικά ερωτηματικά. + +.[warning] +Οι τιμές πρέπει να είναι κλιμακωτού τύπου (`string`, `int`, `float`, `bool`) ή `null`. Αν, για παράδειγμα, `$_GET['name']` είναι ένας πίνακας, η Nette Database θα συμπεριλάβει όλα τα στοιχεία του στο ερώτημα SQL, πράγμα που μπορεί να είναι ανεπιθύμητο. + + +Πίνακες τιμών .[#toc-value-arrays] +---------------------------------- + +Για τις ρήτρες `INSERT`, `UPDATE` ή `WHERE`, μπορούμε να χρησιμοποιήσουμε πίνακες τιμών: + +```php +// ✅ Ασφαλής ΕΙΣΑΓΩΓΗ +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Ασφαλής ΕΝΗΜΕΡΩΣΗ +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Η Nette Database διαφυλάσσει αυτόματα όλες τις τιμές που περνούν μέσω παραμετροποιημένων ερωτημάτων. Ωστόσο, πρέπει να διασφαλίσουμε τον σωστό τύπο δεδομένων των παραμέτρων. + + +Τα κλειδιά συστοιχίας δεν είναι ασφαλές API .[#toc-array-keys-are-not-a-safe-api] +================================================================================= + +Ενώ οι τιμές στους πίνακες είναι ασφαλείς, δεν ισχύει το ίδιο για τα κλειδιά: + +```php +// ❌ UNSAFE CODE - τα κλειδιά μπορεί να περιέχουν SQL injection +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Για τις εντολές `INSERT` και `UPDATE`, αυτό είναι ένα κρίσιμο ελάττωμα ασφαλείας - ένας εισβολέας θα μπορούσε να εισάγει ή να τροποποιήσει οποιαδήποτε στήλη στη βάση δεδομένων. Για παράδειγμα, θα μπορούσε να ορίσει το `is_admin = 1` ή να εισάγει αυθαίρετα δεδομένα σε ευαίσθητες στήλες. + +Σε συνθήκες `WHERE`, αυτό είναι ακόμη πιο επικίνδυνο επειδή επιτρέπει **SQL enumeration** - μια τεχνική για τη σταδιακή ανάκτηση πληροφοριών σχετικά με τη βάση δεδομένων. Ένας επιτιθέμενος θα μπορούσε να επιχειρήσει να εξερευνήσει τους μισθούς των υπαλλήλων εισάγοντας στο `$_GET` με τον ακόλουθο τρόπο: + +```php +$_GET = ['salary >', 100000]; // αρχίζει τον καθορισμό των μισθολογικών κλιμακίων +``` + +Το κύριο πρόβλημα, ωστόσο, είναι ότι οι συνθήκες `WHERE` υποστηρίζουν εκφράσεις SQL στα κλειδιά: + +```php +// Νόμιμη χρήση τελεστών σε κλειδιά +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ UNSAFE: ο επιτιθέμενος μπορεί να εισάγει τη δική του SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // επιτρέπει στον επιτιθέμενο να αποκτήσει μισθούς διαχειριστή +``` + +Αυτό είναι και πάλι **SQL injection**. + + +Λευκή λίστα στηλών .[#toc-whitelisting-columns] +----------------------------------------------- + +Αν θέλετε να επιτρέψετε στους χρήστες να επιλέγουν στήλες, χρησιμοποιείτε πάντα μια λευκή λίστα: + +```php +// ✅ Ασφαλής επεξεργασία - μόνο επιτρεπόμενες στήλες +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Δυναμικά αναγνωριστικά .[#toc-dynamic-identifiers] +================================================== + +Για δυναμικά ονόματα πινάκων και στηλών, χρησιμοποιήστε τον αντικαταστάτη `?name`: + +```php +// ✅ Ασφαλής χρήση αξιόπιστων αναγνωριστικών +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - Ποτέ μην χρησιμοποιείτε είσοδο χρήστη +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Το σύμβολο `?name` πρέπει να χρησιμοποιείται μόνο για αξιόπιστες τιμές που ορίζονται στον κώδικα της εφαρμογής. Για τιμές που παρέχονται από τον χρήστη, χρησιμοποιήστε πάλι μια λευκή λίστα. diff --git a/database/en/@left-menu.texy b/database/en/@left-menu.texy index de015b67a7..77eeeab172 100644 --- a/database/en/@left-menu.texy +++ b/database/en/@left-menu.texy @@ -4,4 +4,5 @@ Database - [Explorer] - [Reflection] - [Configuration] +- [Security Risks |security] - [Upgrading] diff --git a/database/en/explorer.texy b/database/en/explorer.texy index 383f0f82e4..1b2edc619d 100644 --- a/database/en/explorer.texy +++ b/database/en/explorer.texy @@ -3,548 +3,927 @@ Database Explorer
-Nette Database Explorer significantly simplifies retrieving data from the database without writing SQL queries. +Nette Database Explorer is a powerful layer that significantly simplifies data retrieval from the database without the need to write SQL queries. -- uses efficient queries -- no data is transmitted unnecessarily -- features elegant syntax +- Working with data is natural and easy to understand +- Generates optimized SQL queries that fetch only the necessary data +- Provides easy access to related data without the need to write JOIN queries +- Works immediately without any configuration or entity generation
-To use Database Explorer, start with a table - call `table()` on a [api:Nette\Database\Explorer] object. The easiest way to get a context object instance is [described here |core#Connection and Configuration], or, for case when Nette Database Explorer is used as a standalone tool, it can be [created manually|#Creating Explorer Manually]. +Nette Database Explorer is an extension of the low-level [Nette Database Core |core] layer, which adds a convenient object-oriented approach to database management. + +Working with Explorer starts with calling the `table()` method on the [api:Nette\Database\Explorer] object (how to obtain it is [described here |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // db table name is 'book' +$books = $explorer->table('book'); // 'book' is the table name ``` -The call returns an instance of [Selection |api:Nette\Database\Table\Selection] object, that can be iterated over to retrieve all the books. Each item (a row) is represented by an instance of [ActiveRow |api:Nette\Database\Table\ActiveRow] with data mapped to its properties: +The method returns a [Selection |api:Nette\Database\Table\Selection] object, which represents an SQL query. Additional methods can be chained to this object for filtering and sorting results. The query is assembled and executed only when the data is requested, for example, by iterating with `foreach`. Each row is represented by an [ActiveRow |api:Nette\Database\Table\ActiveRow] object: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // outputs 'title' column + echo $book->author_id; // outputs 'author_id' column } ``` -Getting just one specific row is done by `get()` method, which directly returns an ActiveRow instance. +Explorer greatly simplifies working with [table relationships |#Vazby mezi tabulkami]. The following example shows how easily we can output data from related tables (books and their authors). Notice that no JOIN queries need to be written; Nette generates them for us: ```php -$book = $explorer->table('book')->get(2); // returns book with id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // creates a JOIN to the 'author' table +} ``` -Let's take a look at common use-case. You need to fetch books and their authors. It is a common 1:N relationship. The often used solution is to fetch data using one SQL query with table joins. The second possibility is to fetch data separately, run one query for getting books and then get an author for each book by another query (e.g. in your foreach cycle). This could be easily optimized to run only two queries, one for the books, and another for the needed authors - and that is exactly the way how Nette Database Explorer does it. +Nette Database Explorer optimizes queries for maximum efficiency. The above example performs only two SELECT queries, regardless of whether we process 10 or 10,000 books. -In the examples below, we will work with the database schema in the figure. There are OneHasMany (1:N) links (author of book `author_id` and possible translator `translator_id`, which may be `null`) and ManyHasMany (M:N) link between book and its tags. +Additionally, Explorer tracks which columns are used in the code and fetches only those from the database, saving further performance. This behavior is fully automatic and adaptive. If you later modify the code to use additional columns, Explorer automatically adjusts the queries. You don’t need to configure anything or think about which columns will be needed — leave that to Nette. -[An example, including a schema, is found on GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Database structure used in the examples .<> +Filtering and Sorting +===================== -The following code lists the author's name for each book and all its tags. We will [discuss in a moment |#Working with relationships] how this works internally. +The `Selection` class provides methods for filtering and sorting data. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Adds a WHERE condition. Multiple conditions are combined using AND | +| `whereOr(array $conditions)` | Adds a group of WHERE conditions combined using OR | +| `wherePrimary($value)` | Adds a WHERE condition based on the primary key | +| `order($columns, ...$params)` | Sets sorting with ORDER BY | +| `select($columns, ...$params)` | Specifies which columns to fetch | +| `limit($limit, $offset = null)` | Limits the number of rows (LIMIT) and optionally sets OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Sets pagination | +| `group($columns, ...$params)` | Groups rows (GROUP BY) | +| `having($condition, ...$params)`| Adds a HAVING condition for filtering grouped rows | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author is row from table 'author' +Methods can be chained (so-called [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 is row from table 'tag' - } -} -``` +These methods also allow the use of special notations for accessing [data from related tables|#Dotazování přes související tabulky]. -You will be pleased how efficiently the database layer works. The example above makes a constant number of requests that look like this: -```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)) -``` +Escaping and Identifiers +------------------------ -If you use [cache |caching:] (defaults on), no columns will be queried unnecessarily. After the first query, cache will store the used column names and Nette Database Explorer will run queries only with the needed columns: +The methods automatically escape parameters and quote identifiers (table and column names), preventing SQL injection. To ensure proper operation, a few rules must be followed: -```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)) +- Write keywords, function names, procedures, etc., in **uppercase**. +- Write column and table names in **lowercase**. +- Always pass strings using **parameters**. + +```php +where('name = ' . $name); // **DISASTER**: vulnerable to SQL injection +where('name LIKE "%search%"'); // **WRONG**: complicates automatic quoting +where('name LIKE ?', '%search%'); // **CORRECT**: value passed as a parameter + +where('name like ?', $name); // **WRONG**: generates: `name` `like` ? +where('name LIKE ?', $name); // **CORRECT**: generates: `name` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`name`) = ? ``` -Selections -========== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -See possibilities how to filter and restrict rows [api:Nette\Database\Table\Selection]: +Filters results using WHERE conditions. Its strength lies in intelligently handling various value types and automatically selecting SQL operators. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Set WHERE using AND as a glue if two or more conditions are supplied -| `$table->whereOr($where)` | Set WHERE using OR as a glue if two or more conditions are supplied -| `$table->order($columns)` | Set ORDER BY, can be expression `('column DESC, id DESC')` -| `$table->select($columns)` | Set retrieved columns, can be expression `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Set LIMIT and OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Enables pagination -| `$table->group($columns)` | Set GROUP BY -| `$table->having($having)` | Set HAVING +Basic usage: -We can use a so-called [fluent interface |nette:introduction-to-object-oriented-programming#fluent-interfaces], for example `$table->where(...)->order(...)->limit(...)`. Multiple `where` or `whereOr` conditions are linked by the `AND` operator. +```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' +``` +Thanks to automatic detection of suitable operators, you don’t need to handle special cases — Nette handles them for you: -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) +// Placeholder ? can be used without an operator: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer can automatically add needed operators for passed values: +The method also handles negative conditions and empty arrays correctly: -.[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 -- finds nothing +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- finds everything +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- finds everything +// $table->where('NOT id ?', $ids); // WARNING: This syntax is not supported +``` -You can provide placeholder even without column operator. These calls are the same. +You can also pass the result of another table query as a parameter, creating a subquery: ```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')); ``` -This feature allows to generate correct operator based on value: +Conditions can also be passed as an array, with the items combined using 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 correctly handles also negative conditions, works for empty arrays too: +In the array, key-value pairs can be used, and Nette will again automatically choose the correct operators: ```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], +]); +``` + +We can also mix SQL expressions with placeholders and multiple parameters. This is useful for complex conditions with precisely defined operators: -// this will throws an exception, this syntax is not supported -$table->where('NOT id ?', $ids); +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // two parameters are passed as an array +]); ``` +Multiple calls to `where()` automatically combine conditions using AND. -whereOr() ---------- -Example of use without parameters: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Similar to `where()`, but combines conditions using 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, ]); ``` -We use the parameters. If you do not specify an operator, Nette Database Explorer will automatically add the appropriate one: +More complex expressions can also be used: ```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, ]); ``` -The key can contain an expression containing wildcard question marks and then pass parameters in the value: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Adds a condition for the table's primary key: ```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]); +``` + +If the table has a composite primary key (e.g., `foo_id`, `bar_id`), we pass it as an array: + +```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] +-------------------------------------------------------- -Examples of use: +Specifies the order in which rows are returned. You can sort by one or more columns, in ascending or descending order, or by a custom expression: ```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] +--------------------------------------------------------- -Examples of use: +Specifies the columns to be returned from the database. By default, Nette Database Explorer returns only the columns that are actually used in the code. Use the `select()` method when you need to retrieve specific expressions: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Aliases defined using `AS` are then accessible as properties of the `ActiveRow` object: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // access the alias +} +``` -limit() -------- -Examples of use: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Limits the number of rows returned (LIMIT) and optionally sets an offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (returns the first 10 rows) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +For pagination, it is more appropriate to use the `page()` method. + -page() ------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -An alternative way to set the limit and offset: +Simplifies the pagination of results. It accepts the page number (starting from 1) and the number of items per page. Optionally, you can pass a reference to a variable where the total number of pages will be stored: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Getting the last page number, passed to the `$lastPage` variable: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Groups rows by the specified columns (GROUP BY). It is typically used in combination with aggregate functions: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Counts the number of products in each category +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() -------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Examples of use: +Sets a condition for filtering grouped rows (HAVING). It can be used in combination with the `group()` method and aggregate functions: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Finds categories with more than 100 products +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() --------- +Reading Data +============ -Examples of use: +For reading data from the database, several useful methods are available: + +.[language-php] +| `foreach ($table as $key => $row)` | Iterates through all rows, `$key` is the primary key value, `$row` is an ActiveRow object | +| `$row = $table->get($key)` | Returns a single row by primary key | +| `$row = $table->fetch()` | Returns the current row and advances the pointer to the next one | +| `$array = $table->fetchPairs()` | Creates an associative array from the results | +| `$array = $table->fetchAll()` | Returns all rows as an array | +| `count($table)` | Returns the number of rows in the Selection object | + +The [ActiveRow |api:Nette\Database\Table\ActiveRow] object is read-only. This means you cannot change its properties' values. This restriction ensures data consistency and prevents unexpected side effects. Data is fetched from the database, and any changes should be made explicitly and in a controlled manner. + + +`foreach` - Iterating Through All Rows +-------------------------------------- + +The easiest way to execute a query and retrieve rows is by iterating with the `foreach` loop. It automatically executes the SQL query. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = primary key, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtering by Another Table Value .[#toc-joining-key] ----------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Executes an SQL query and returns a row by its primary key or `null` if it does not exist. + +```php +$book = $explorer->table('book')->get(123); // returns ActiveRow with ID 123 or null +if ($book) { + echo $book->title; +} +``` + -Quite often you need filter results by some condition which involves another database table. These types of condition require table join. However, you don't need to write them anymore. +fetch(): ?ActiveRow .[method] +----------------------------- -Let's say you need to get all books whose author's name is 'Jon'. All you need to write is the joining key of the relation and the column name in the joined table. The joining key is derived from the column which refers to the table you want to join. In our example (see the db schema) it is the column `author_id`, and it is sufficient to use just the first part of it - `author` (the `_id` suffix can be omitted). `name` is a column in the `author` table we would like to use. A condition for book translator (which is connected by `translator_id` column) can be created just as easily. +Returns one row and advances the internal pointer to the next one. If there are no more rows, it returns `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); +} ``` -The joining key logic is driven by implementation of [Conventions |api:Nette\Database\Conventions]. We encourage to use [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], which analyzes your foreign keys and allows you to easily work with these relationships. -The relationship between book and its author is 1:N. The reverse relationship is also possible. We call it **backjoin**. Take a look at another example. We would like to fetch all authors, who have written more than 3 books. To make the join reverse we use `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY` statement, also the condition has to be written in form of `HAVING` statement. +fetchPairs(): array .[method] +----------------------------- + +Returns the results as an associative array. The first argument specifies the column name to be used as the key in the array, and the second argument specifies the column name to be used as the value: ```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', ...] ``` -You may have noticed that the joining expression refers to the book, but it's not clear, whether we are joining through `author_id` or `translator_id`. In the example above, Selection joins through the `author_id` column because a match with the source table has been found - the `author` table. If there was no such a match and there would be more possibilities, Nette would throw [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +If only the key column is specified, the value will be the entire row, i.e., the `ActiveRow` object: -To make a join through `translator_id` column, provide an optional parameter within the joining expression. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +If `null` is specified as the key, the array will be numerically indexed starting from zero: ```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', ...] ``` -Let's take a look at some more difficult joining expression. +You can also pass a callback as a parameter, which will return either the value itself or a key-value pair for each row. If the callback returns only a value, the key will be the row's primary key: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'First Book (Jan Novak)', ...] + +// The callback can also return an array with a key & value pair: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['First Book' => 'Jan Novak', ...] +``` -We would like to find all authors who have written something about PHP. All books have tags so we should select those authors who have written any book with the PHP tag. + +fetchAll(): array .[method] +--------------------------- + +Returns all rows as an associative array of `ActiveRow` objects, where the keys are the primary key values. ```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, ...), ...] ``` -Aggregate Queries ------------------ +count(): int .[method] +---------------------- -| `$table->count('*')` | Get number of rows -| `$table->count("DISTINCT $column")` | Get number of distinct values -| `$table->min($column)` | Get minimum value -| `$table->max($column)` | Get maximum value -| `$table->sum($column)` | Get the sum of all values -| `$table->aggregation("GROUP_CONCAT($column)")` | Run any aggregation function +The `count()` method without parameters returns the number of rows in the `Selection` object: + +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternative +``` + +Note: `count()` with a parameter performs the COUNT aggregation function in the database, as described below. + + +ActiveRow::toArray(): array .[method] +------------------------------------- + +Converts the `ActiveRow` object into an associative array where keys are column names and values are the corresponding data. + +```php +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray will be ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` -.[caution] -The `count()` method without any specified parameters selects all records and returns the array size, which is very inefficient. For example, if you need to calculate the number of rows for paging, always specify the first argument. +Aggregation +=========== -Escaping & Quoting -================== +The `Selection` class provides methods for easily performing aggregation functions (COUNT, SUM, MIN, MAX, AVG, etc.). + +.[language-php] +| `count($expr)` | Counts the number of rows | +| `min($expr)` | Returns the minimum value in a column | +| `max($expr)` | Returns the maximum value in a column | +| `sum($expr)` | Returns the sum of values in a column | +| `aggregation($function)` | Allows any aggregation function, such as `AVG()` or `GROUP_CONCAT()` | -Database Explorer is smart and escape parameters and quotes identificators for you. These basic rules need to be followed, though: -- keywords, functions, procedures must be uppercase -- columns and tables must be lowercase -- pass variables as parameters, do not concatenate +count(string $expr): int .[method] +---------------------------------- + +Executes an SQL query with the COUNT function and returns the result. This method is used to determine how many rows match a certain condition: ```php -->where('name like ?', 'John'); // WRONG! generates: `name` `like` ? -->where('name LIKE ?', 'John'); // CORRECT +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Note: [#count()] without a parameter simply returns the number of rows in the `Selection` object. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +The `min()` and `max()` methods return the minimum and maximum values in the specified column or expression: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` -->where('KEY = ?', $value); // WRONG! KEY is a keyword -->where('key = ?', $value); // CORRECT. generates: `key` = ? -->where('name = ' . $name); // WRONG! sql injection! -->where('name = ?', $name); // CORRECT +sum(string $expr): int .[method] +-------------------------------- -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // WRONG! pass variables as parameters, do not concatenate -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORRECT +Returns the sum of values in the specified column or expression: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -Wrong usage can produce security holes +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Allows the execution of any aggregation function. + +```php +// Calculates the average price of products in a category +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -Fetching Data -============= +// Combines product tags into a single string +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` -| `foreach ($table as $id => $row)` | Iterate over all rows in result -| `$row = $table->get($id)` | Get single row with ID $id from table -| `$row = $table->fetch()` | Get next row from the result -| `$array = $table->fetchPairs($key, $value)` | Fetch all values to associative array -| `$array = $table->fetchPairs($value)` | Fetch all rows to associative array -| `count($table)` | Get number of rows in result set +If we need to aggregate results that themselves result from an aggregation and grouping (e.g., `SUM(value)` over grouped rows), we specify the aggregation function to be applied to these intermediate results as the second argument: + +```php +// Calculates the total price of products in stock for each category, then sums these prices +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +In this example, we first calculate the total price of products in each category (`SUM(price * stock) AS category_total`) and group the results by `category_id`. We then use `aggregation('SUM(category_total)', 'SUM')` to sum these subtotals. The second argument `'SUM'` specifies the aggregation function to apply to intermediate results. Insert, Update & Delete ======================= -Method `insert()` accepts array or Traversable objects (for example [ArrayHash |utils:arrays#ArrayHash] which returns [forms|forms:]): +Nette Database Explorer simplifies inserting, updating, and deleting data. All the methods mentioned throw a `Nette\Database\DriverException` in case of an error. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Inserts new records into a table. + +**Inserting a single record:** + +The new record is passed as an associative array or iterable object (such as `ArrayHash` used in [forms |forms:]), where the keys match the column names in the table. + +If the table has a defined primary key, the method returns an `ActiveRow` object, which is reloaded from the database to reflect any changes made at the database level (e.g., triggers, default column values, or auto-increment calculations). This ensures data consistency, and the object always contains the current database data. If a primary key is not explicitly defined, the method returns the input data as an array. ```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 is an instance of ActiveRow containing the full data of the inserted row, +// including auto-generated ID and any changes made by triggers +echo $row->id; // Outputs the ID of the newly inserted user +echo $row->created_at; // Outputs the creation time if set by a trigger ``` -If primary key is defined on the table, an ActiveRow object containing the inserted row is returned. +**Inserting multiple records at once:** -Multiple insert: +The `insert()` method allows you to insert multiple records with a single SQL query. In this case, it returns the number of inserted rows. ```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 will be 2 ``` -Files or DateTime objects can be passed as parameters: +You can also pass a `Selection` object with a selection of data as a parameter. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Inserting special values:** + +Values can include files, `DateTime` objects, or SQL literals: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // converts to the database format + 'avatar' => fopen('image.jpg', 'rb'), // inserts binary file content + 'uuid' => $explorer::literal('UUID()'), // calls the UUID() function ]); ``` -Updating (returns the count of affected rows): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Updates rows in a table based on a specified filter. Returns the number of rows actually modified. + +The columns to be updated are passed as an associative array or iterable object (such as `ArrayHash` used in [forms |forms:]), where the keys match the column names in the table: ```php -$count = $explorer->table('users') - ->where('id', 10) // must be called before 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 ``` -For update we can use operators `+=` a `-=`: +To change numeric values, you can use the `+=` and `-=` operators: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // increases the value of the 'points' column by 1 + 'coins-=' => 1, // decreases the value of the 'coins' column by 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Deleting (returns the count of deleted rows): + +Selection::delete(): int .[method] +---------------------------------- + +Deletes rows from a table based on a specified filter. Returns the number of deleted rows. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +When calling `update()` or `delete()`, be sure to use `where()` to specify the rows to be updated or deleted. If `where()` is not used, the operation will be performed on the entire table! + -Working with Relationships -========================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Updates the data in a database row represented by the `ActiveRow` object. It accepts iterable data as a parameter, where the keys are column names. To change numeric values, you can use the `+=` and `-=` operators: -Has One Relation ----------------- -Has one relation is a common use-case. Book *has one* author. Book *has one* translator. Getting related row is mainly done by `ref()` method. It accepts two arguments: target table name and source joining column. See example: +After the update is performed, the `ActiveRow` is automatically reloaded from the database to reflect any changes made at the database level (e.g., triggers). The method returns `true` only if a real data change occurred. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // increments the view count +]); +echo $article->views; // Outputs the current view count ``` -In example above we fetch related author entry from `author` table, the author primary key is searched by `book.author_id` column. Ref() method returns ActiveRow instance or null if there is no appropriate entry. Returned row is an instance of ActiveRow so we can work with it the same way as with the book entry. +This method updates only one specific row in the database. For bulk updates of multiple rows, use the [#Selection::update()] method. + + +ActiveRow::delete() .[method] +----------------------------- + +Deletes a row from the database that is represented by the `ActiveRow` object. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Deletes the book with ID 1 +``` + +This method deletes only one specific row in the database. For bulk deletion of multiple rows, use the [#Selection::delete()] method. + -// or directly -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Relationships Between Tables +============================ + +In relational databases, data is split into multiple tables and connected through foreign keys. Nette Database Explorer offers a revolutionary way to work with these relationships — without writing JOIN queries or requiring any configuration or entity generation. + +For demonstration, we'll use the **example database** ([available on GitHub |https://github.com/nette-examples/books]). The database includes the following tables: + +- `author` – authors and translators (columns `id`, `name`, `web`, `born`) +- `book` – books (columns `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` – tags (columns `id`, `name`) +- `book_tag` – link table between books and tags (columns `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Database structure .<> + +In this book database example, we find several types of relationships (simplified compared to reality): + +- **One-to-many (1:N)** – Each book **has one** author; an author can write **multiple** books. +- **Zero-to-many (0:N)** – A book **can have** a translator; a translator can translate **multiple** books. +- **Zero-to-one (0:1)** – A book **can have** a sequel. +- **Many-to-many (M:N)** – A book **can have several** tags, and a tag can be assigned to **several** books. + +In these relationships, there is always a **parent table** and a **child table**. For example, in the relationship between authors and books, the `author` table is the parent, and the `book` table is the child — you can think of it as a book always "belonging" to an author. This is also reflected in the database structure: the child table `book` contains the foreign key `author_id`, which references the parent table `author`. + +If we want to display books along with their authors' names, we have two options. Either we retrieve the data using a single SQL query with a JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Book also has one translator, so getting translator name is quite easy. +Or we retrieve the data in two steps — first the books, then their authors — and assemble them in PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books +``` + +The second approach is, surprisingly, **more efficient**. The data is fetched only once and can be better utilized in cache. This is exactly how Nette Database Explorer works — it handles everything under the hood and provides you with a clean 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 is a record from the 'author' table + echo 'translated by: ' . $book->translator?->name; +} ``` -All of this is fine, but it's somewhat cumbersome, don't you think? Database Explorer already contains the foreign keys definitions so why not use them automatically? Let's do that! -If we call property, which does not exist, ActiveRow tries to resolve the calling property name as 'has one' relation. Getting this property is the same as calling ref() method with just one argument. We will call the only argument the **key**. Key will be resolved to particular foreign key relation. The passed key is matched against row columns, and if it matches, foreign key defined on the matched column is used for getting data from related target table. See example: +Accessing the Parent Table +-------------------------- + +Accessing the parent table is straightforward. These are relationships like *a book has an author* or *a book may have a translator*. The related record can be accessed through the `ActiveRow` object property — the property name matches the column name of the foreign key without the `id` suffix: ```php -$book->author->name; -// same as -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // finds the author via the 'author_id' column +echo $book->translator?->name; // finds the translator via the 'translator_id' column ``` -The ActiveRow instance has no author column. All book columns are searched for a match with *key*. Matching in this case means the column name has to contain the key. So in the example above, the `author_id` column contains string 'author' and is therefore matched by 'author' key. If you want to get the book translator, just can use e.g. 'translator' as a key, because 'translator' key will match the `translator_id` column. You can find more about the key matching logic in [Joining expressions |#joining-key] chapter. +When accessing the `$book->author` property, Explorer looks for a column in the `book` table that contains the string `author` (i.e., `author_id`). Based on the value in this column, it retrieves the corresponding record from the `author` table and returns it as an `ActiveRow` object. Similarly, `$book->translator` uses the `translator_id` column. Since the `translator_id` column can contain `null`, the `?->` operator is used. + +An alternative approach is provided by the `ref()` method, which accepts two arguments — the name of the target table and the linking column — and returns an `ActiveRow` instance or `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; // link to the author +echo $book->ref('author', 'translator_id')->name; // link to the translator ``` -If you want to fetch multiple books, you should use the same approach. Nette Database Explorer will fetch authors and translators for all the fetched books at once. +The `ref()` method is useful if property-based access cannot be used, for example, when the table contains a column with the same name as the property (`author`). In other cases, using property-based access is recommended for better readability. + +Explorer automatically optimizes database queries. When iterating through books and accessing their related records (authors, translators), Explorer does not generate a query for each book individually. Instead, it executes only **one SELECT query for each type of relationship**, significantly reducing the database load. For example: ```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; } ``` -The code will run only these 3 queries: +This code will execute only three optimized database queries: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +The logic for identifying the linking column is defined by the implementation of [Conventions |api:Nette\Database\Conventions]. We recommend using [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], which analyzes foreign keys and allows you to work seamlessly with existing table relationships. -Has Many Relation ------------------ -'Has many' relation is just reversed 'has one' relation. Author *has* written *many* books. Author *has* translated *many* books. As you can see, this type of relation is a little bit more difficult because the relation is 'named' ('written', 'translated'). ActiveRow instance has `related()` method, which will return array of related entries. Entries are also ActiveRow instances. See example bellow: +Accessing the Child Table +------------------------- + +Accessing the child table works in the opposite direction. Now we ask *which books did this author write* or *which books did this translator translate*. For this type of query, we use the `related()` method, which returns a `Selection` object with related records. Here's an example: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Outputs all books written by the author foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Outputs all books translated by the author foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Method `related()` method accepts full join description passed as two arguments or as one argument joined by dot. The first argument is the target table, the second is the target column. +The `related()` method accepts the relationship description as a single argument using dot notation or as two separate arguments: ```php -$author->related('book.translator_id'); -// same as -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // single argument +$author->related('book', 'translator_id'); // two arguments ``` -You can use Nette Database Explorer heuristics based on foreign keys and provide only **key** argument. Key will be matched against all foreign keys pointing into the current table (`author` table). If there is a match, Nette Database Explorer will use this foreign key, otherwise it will throw [Nette\InvalidArgumentException|api:Nette\InvalidArgumentException] or [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. You can find more about the key matching logic in [Joining expressions |#joining-key] chapter. +Explorer can automatically detect the correct linking column based on the name of the parent table. In this case, it links via the `book.author_id` column because the name of the source table is `author`: -Of course you can call related methods for all fetched authors, Nette Database Explorer will again fetch the appropriate books at once. +```php +$author->related('book'); // uses book.author_id +``` + +If multiple possible connections exist, Explorer will throw an exception [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +We can, of course, also use the `related()` method when iterating through multiple records in a loop, and Explorer will automatically optimize the queries in this case as well: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Example above will run only two queries: +This code generates only two efficient SQL queries: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Creating Explorer Manually -========================== +Many-to-Many Relationship +------------------------- + +For a many-to-many (M:N) relationship, a **junction table** (in our case, `book_tag`) is required. This table contains two foreign key columns (`book_id`, `tag_id`). Each column references the primary key of one of the connected tables. To retrieve related data, we first fetch records from the link table using `related('book_tag')`, and then continue to the target data: + +```php +$book = $explorer->table('book')->get(1); +// Outputs the names of tags assigned to the book +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // fetches tag name through the link table +} + +$tag = $explorer->table('tag')->get(1); +// Opposite direction: outputs the titles of books with this tag +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // fetches book title +} +``` + +Explorer again optimizes the SQL queries into an efficient form: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + -A database connection can be created using the application configuration. In such cases a `Nette\Database\Explorer` service is created and can be passed as a dependency using the DI container. +Querying Through Related Tables +------------------------------- -However, if Nette Database Explorer is used as a standalone tool, an instance of `Nette\Database\Explorer` object needs to be created manually. +In the methods `where()`, `select()`, `order()`, and `group()`, you can use special notations to access columns from other tables. Explorer automatically creates the required JOINs. + +**Dot notation** (`parent_table.column`) is used for 1:N relationships as viewed from the parent table's perspective: ```php -// $storage implements Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Finds books whose authors' names start with 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Sorts books by author name descending +$books->order('author.name DESC'); + +// Outputs book title and author name +$books->select('book.title, author.name'); +``` + +**Colon notation** is used for 1:N relationships from the perspective of the parent table: + +```php +$authors = $explorer->table('author'); + +// Finds authors who wrote a book with 'PHP' in the title +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Counts the number of books for each author +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +In the above example with colon notation (`:book.title`), the foreign key column is not explicitly specified. Explorer automatically detects the correct column based on the parent table name. In this case, it joins through the `book.author_id` column because the name of the source table is `author`. If multiple possible connections exist, Explorer throws the exception [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +The linking column can be explicitly specified in parentheses: + +```php +// Finds authors who translated a book with 'PHP' in the title +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Notations can be chained to access data across multiple tables: + +```php +// Finds authors of books tagged with 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Extending Conditions for JOIN +----------------------------- + +The `joinWhere()` method adds additional conditions to table joins in SQL after the `ON` keyword. + +For example, let's say we want to find books translated by a specific translator: + +```php +// Finds books translated by a translator named 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +In the `joinWhere()` condition, you can use the same constructs as in the `where()` method — operators, placeholders, arrays of values, or SQL expressions. + +For more complex queries with multiple JOINs, table aliases can be defined: + +```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) +``` + +Note that while the `where()` method adds conditions to the `WHERE` clause, the `joinWhere()` method extends the conditions in the `ON` clause during table joins. + + +Manually Creating Explorer +========================== + +If you are not using the Nette DI container, you can create an instance of `Nette\Database\Explorer` manually: + +```php +use Nette\Database; + +// $storage implements Nette\Caching\Storage, e.g.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// database connection +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// manages reflection of the database structure +$structure = new Database\Structure($connection, $storage); +// defines rules for mapping table names, columns, and foreign keys +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/en/security.texy b/database/en/security.texy new file mode 100644 index 0000000000..04e765324d --- /dev/null +++ b/database/en/security.texy @@ -0,0 +1,145 @@ +Security Risks +************** + +.[perex] +Databases often contain sensitive data and allow for dangerous operations. Nette Database provides a number of security features. However, it is crucial to understand the difference between safe and unsafe APIs. + + +SQL Injection +============= + +SQL injection is the most serious security risk when working with databases. It occurs when unchecked user input becomes part of an SQL query. An attacker can inject their own SQL commands, gaining or modifying data in the database. + +```php +// ❌ UNSAFE CODE - vulnerable to SQL injection +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// The attacker can input something like: ' OR '1'='1 +// The resulting query will be: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// This returns all users! +``` + +The same applies to Database Explorer: + +```php +// ❌ UNSAFE CODE +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Safe Parameterized Queries +========================== + +The safe way to insert values into SQL queries is through parameterized queries. Nette Database provides several ways to use them. + + +Placeholder Question Marks +-------------------------- + +The simplest method is to use placeholder question marks: + +```php +// ✅ Safe parameterized queries +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Safe condition in Explorer +$table->where('name = ?', $_GET['name']); +``` + +The same applies to all other methods in Database Explorer that allow inserting expressions with placeholder question marks and parameters. + +.[warning] +Values must be of scalar type (`string`, `int`, `float`, `bool`) or `null`. If, for example, `$_GET['name']` is an array, Nette Database will include all of its elements in the SQL query, which can be undesirable. + + +Value Arrays +------------ + +For `INSERT`, `UPDATE`, or `WHERE` clauses, we can use value arrays: + +```php +// ✅ Safe INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Safe UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database automatically escapes all values passed through parameterized queries. However, we must ensure the correct data type of parameters. + + +Array Keys Are Not a Safe API +============================= + +While values in arrays are safe, the same cannot be said for keys: + +```php +// ❌ UNSAFE CODE - keys can contain SQL injection +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +For `INSERT` and `UPDATE` commands, this is a critical security flaw – an attacker could insert or modify any column in the database. For example, they could set `is_admin = 1` or insert arbitrary data into sensitive columns. + +In `WHERE` conditions, this is even more dangerous because it allows **SQL enumeration** – a technique to gradually retrieve information about the database. An attacker could attempt to explore employee salaries by injecting into `$_GET` like this: + +```php +$_GET = ['salary >', 100000]; // starts determining salary ranges +``` + +The main problem, however, is that `WHERE` conditions support SQL expressions in keys: + +```php +// Legitimate use of operators in keys +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ UNSAFE: attacker can inject their own SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // allows the attacker to obtain admin salaries +``` + +This is once again **SQL injection**. + + +Whitelisting Columns +-------------------- + +If you want to allow users to choose columns, always use a whitelist: + +```php +// ✅ Safe processing - only allowed columns +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dynamic Identifiers +=================== + +For dynamic table and column names, use the `?name` placeholder: + +```php +// ✅ Safe use of trusted identifiers +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - never use user input +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +The `?name` symbol should only be used for trusted values defined in the application code. For user-provided values, use a whitelist again. diff --git a/database/es/@left-menu.texy b/database/es/@left-menu.texy index 71215e5f94..fa457f85e3 100644 --- a/database/es/@left-menu.texy +++ b/database/es/@left-menu.texy @@ -4,3 +4,4 @@ Base de datos - [Explorador |Explorer] - [Reflexión |Reflection] - [Configuración |Configuration] +- [Riesgos de seguridad |security] diff --git a/database/es/explorer.texy b/database/es/explorer.texy index 00adca3494..eabc49d394 100644 --- a/database/es/explorer.texy +++ b/database/es/explorer.texy @@ -3,548 +3,927 @@ Explorador de bases de datos
-Nette Database Explorer simplifica significativamente la recuperación de datos de la base de datos sin escribir consultas SQL. +Nette Database Explorer es una potente capa que simplifica significativamente la recuperación de datos de la base de datos sin necesidad de escribir consultas SQL. -- utiliza consultas eficientes -- no se transmiten datos innecesariamente -- presenta una sintaxis elegante +- Trabajar con datos es natural y fácil de entender +- Genera consultas SQL optimizadas que obtienen sólo los datos necesarios +- Facilita el acceso a datos relacionados sin necesidad de escribir consultas JOIN. +- Funciona inmediatamente sin necesidad de configuración ni generación de entidades
-Para utilizar Database Explorer, comience con una tabla - llame a `table()` en un objeto [api:Nette\Database\Explorer]. La forma más sencilla de obtener una instancia de objeto de contexto se [describe aquí |core#Connection and Configuration], o, para el caso en que Nette Database Explorer se utilice como herramienta independiente, puede [crearse manualmente |#Creating Explorer Manually]. +Nette Database Explorer es una extensión de la capa de bajo nivel [Nette Database Core |core], que añade un cómodo enfoque orientado a objetos a la gestión de bases de datos. + +El trabajo con Explorer comienza con la llamada al método `table()` en el objeto [api:Nette\Database\Explorer] ( [aquí se describe |core#Connection and Configuration] cómo obtenerlo): ```php -$books = $explorer->table('book'); // db table name is 'book' +$books = $explorer->table('book'); // 'libro' es el nombre de la tabla ``` -La llamada devuelve una instancia del objeto [Selección |api:Nette\Database\Table\Selection], sobre el que se puede iterar para recuperar todos los libros. Cada elemento (una fila) está representado por una instancia de [ActiveRow |api:Nette\Database\Table\ActiveRow] con datos asignados a sus propiedades: +El método devuelve un objeto [Selección |api:Nette\Database\Table\Selection], que representa una consulta SQL. Se pueden encadenar métodos adicionales a este objeto para filtrar y ordenar los resultados. La consulta se monta y ejecuta sólo cuando se solicitan los datos, por ejemplo, iterando con `foreach`. Cada fila está representada por un objeto [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // produce la columna "title + echo $book->author_id; // produce la columna "author_id } ``` -La obtención de una fila específica se realiza mediante el método `get()`, que devuelve directamente una instancia de ActiveRow. +Explorer simplifica enormemente el trabajo con las [relaciones entre tablas |#Vazby mezi tabulkami]. El siguiente ejemplo muestra con qué facilidad podemos obtener datos de tablas relacionadas (libros y sus autores). Observe que no es necesario escribir consultas JOIN; Nette las genera por nosotros: ```php -$book = $explorer->table('book')->get(2); // returns book with id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // crea un JOIN a la tabla 'author +} ``` -Veamos un caso de uso común. Necesita obtener libros y sus autores. Es una relación común 1:N. La solución más utilizada es obtener los datos mediante una consulta SQL con uniones de tablas. La segunda posibilidad es obtener los datos por separado, ejecutar una consulta para obtener los libros y luego obtener un autor para cada libro mediante otra consulta (por ejemplo, en su ciclo foreach). Esto podría optimizarse fácilmente para ejecutar sólo dos consultas, una para los libros y otra para los autores necesarios, y así es exactamente como lo hace Nette Database Explorer. +Nette Database Explorer optimiza las consultas para obtener la máxima eficacia. El ejemplo anterior sólo realiza dos consultas SELECT, independientemente de si procesamos 10 o 10.000 libros. -En los ejemplos siguientes, trabajaremos con el esquema de base de datos de la figura. Hay enlaces OneHasMany (1:N) (autor del libro `author_id` y posible traductor `translator_id`, que puede ser `null`) y enlaces ManyHasMany (M:N) entre el libro y sus etiquetas. +Además, Explorer realiza un seguimiento de las columnas que se utilizan en el código y obtiene sólo esas de la base de datos, lo que ahorra aún más rendimiento. Este comportamiento es totalmente automático y adaptable. Si más adelante modifica el código para utilizar columnas adicionales, Explorer ajusta automáticamente las consultas. No es necesario configurar nada ni pensar qué columnas se necesitarán, eso déjelo en manos de Nette. -[En GitHub se puede encontrar un ejemplo, que incluye un esquema |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Estructura de la base de datos utilizada en los ejemplos .<> +Filtrado y ordenación .[#toc-filtering-and-sorting] +=================================================== -El siguiente código lista el nombre del autor de cada libro y todas sus etiquetas. [Discutiremos en un |#Working with relationships] momento cómo funciona esto internamente. +La clase `Selection` proporciona métodos para filtrar y ordenar datos. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Añade una condición WHERE. Las condiciones múltiples se combinan usando AND. +| `whereOr(array $conditions)` | Añade un grupo de condiciones WHERE combinadas mediante OR. +| `wherePrimary($value)` | Añade una condición WHERE basada en la clave primaria. +| `order($columns, ...$params)` | Establece la ordenación con ORDER BY | +| `select($columns, ...$params)` | Especifica qué columnas obtener | +| `limit($limit, $offset = null)` | Limita el número de filas (LIMIT) y, opcionalmente, establece OFFSET. +| `page($page, $itemsPerPage, &$total = null)` | Establece la paginación. +| `group($columns, ...$params)` | Agrupa las filas (GROUP BY) | +| `having($condition, ...$params)` | Añade una condición HAVING para filtrar las filas agrupadas. -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author is row from table 'author' +Los métodos pueden encadenarse ( [interfaz fluida |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 is row from table 'tag' - } -} -``` +Estos métodos también permiten utilizar notaciones especiales para acceder a [datos de tablas relacionadas |#Dotazování přes související tabulky]. -Le agradará la eficiencia con la que funciona la capa de base de datos. El ejemplo anterior hace un número constante de peticiones que se parecen a esto: -```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)) -``` +Escapes e identificadores .[#toc-escaping-and-identifiers] +---------------------------------------------------------- -Si utiliza [la caché |caching:] (activada por defecto), ninguna columna será consultada innecesariamente. Después de la primera consulta, la caché almacenará los nombres de las columnas utilizadas y Nette Database Explorer ejecutará consultas sólo con las columnas necesarias: +Los métodos escapan automáticamente los parámetros y los identificadores de comillas (nombres de tablas y columnas), evitando la inyección SQL. Para garantizar un funcionamiento correcto, deben seguirse algunas reglas: -```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)) +- Escriba las palabras clave, nombres de funciones, procedimientos, etc., en **mayúsculas**. +- Escriba los nombres de columnas y tablas en **minúsculas**. +- Pasar siempre cadenas utilizando **parámetros**. + +```php +where('name = ' . $name); // **DISASTER**: vulnerable a la inyección SQL +where('name LIKE "%search%"'); // **WRONG**: complica el entrecomillado automático +where('name LIKE ?', '%search%'); // **CORRECTO**: valor pasado como parámetro + +where('name like ?', $name); // **WRONG**: genera: `nombre` `como` ? +where('name LIKE ?', $name); // **CORRECTO**: genera: `name` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECTO**: LOWER(`nombre`) = ? ``` -Selecciones .[#toc-selections] -============================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Ver posibilidades cómo filtrar y restringir filas [api:Nette\Database\Table\Selection]: +Filtra los resultados utilizando condiciones WHERE. Su punto fuerte es el manejo inteligente de varios tipos de valores y la selección automática de operadores SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Establecer WHERE usando AND si se dan dos o más condiciones -| `$table->whereOr($where)` | Establecer WHERE usando OR como pegamento si se suministran dos o más condiciones -| `$table->order($columns)` Establecer ORDER BY, puede ser una expresión. `('column DESC, id DESC')` -| `$table->select($columns)` Establecer columnas recuperadas, puede ser una expresión. `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Establecer LIMIT y OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Habilita la paginación -| `$table->group($columns)` Establecer GROUP BY -| `$table->having($having)` Establecer HAVING +Uso básico: -Podemos utilizar una [interfaz fluida |nette:introduction-to-object-oriented-programming#fluent-interfaces], por ejemplo `$table->where(...)->order(...)->limit(...)`. Varias condiciones `where` o `whereOr` se enlazan mediante el operador `AND`. +```php +$table->where('id', $value); // DONDE `id` = 123 +$table->where('id > ?', $value); // DONDE `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' +``` +Gracias a la detección automática de operadores adecuados, no tendrá que ocuparse de casos especiales: Nette los gestiona por usted: -donde() .[#toc-where] ---------------------- +```php +$table->where('id', 1); // DONDE `id` = 1 +$table->where('id', null); // DONDE `id` ES NULO +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +// El marcador de posición ? puede utilizarse sin operador: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer puede añadir automáticamente los operadores necesarios para los valores pasados: +El método también gestiona correctamente las condiciones negativas y las matrices vacías: -.[language-php] -| `$table->where('field', $value)` Campo = $valor -| `$table->where('field', null)` Campo IS NULL -| `$table->where('field > ?', $val)` Campo > $valor -| `$table->where('field', [1, 2])` | campo EN (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 -- no encuentra nada +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- lo encuentra todo +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- encuentra todo +// $table->where('NOT id ?', $ids); // ADVERTENCIA: Esta sintaxis no está soportada +``` -Puede proporcionar un marcador de posición incluso sin el operador de columna. Estas llamadas son las mismas. +También puede pasar el resultado de otra consulta de tabla como parámetro, creando una subconsulta: ```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')); ``` -Esta función permite generar el operador correcto en función del valor: +Las condiciones también pueden pasarse como una matriz, combinando los elementos mediante 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', +]); ``` -Selección maneja correctamente también las condiciones negativas, funciona para matrices vacías también: +En la matriz pueden utilizarse pares clave-valor, y Nette volverá a elegir automáticamente los operadores correctos: ```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], +]); +``` -// this will throws an exception, this syntax is not supported -$table->where('NOT id ?', $ids); +También podemos mezclar expresiones SQL con marcadores de posición y múltiples parámetros. Esto es útil para condiciones complejas con operadores definidos con precisión: + +```php +// WHERE (`edad` > 18) AND (ROUND(`puntuación`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // se pasan dos parámetros en forma de matriz +]); ``` +Las llamadas múltiples a `where()` combinan automáticamente las condiciones utilizando AND. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Ejemplo de uso sin parámetros: +Similar a `where()`, pero combina condiciones utilizando 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, ]); ``` -Se utilizan los parámetros. Si no especifica un operador, Nette Database Explorer añadirá automáticamente el apropiado: +También se pueden utilizar expresiones más complejas: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`precio` > 1000) OR (`precio_con_impuesto` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -La clave puede contener una expresión que contenga signos de interrogación comodín y luego pasar parámetros en el valor: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Añade una condición para la clave primaria de la tabla: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// DONDE `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Si la tabla tiene una clave primaria compuesta (por ejemplo, `foo_id`, `bar_id`), la pasamos como una matriz: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Ejemplos de uso: +Especifica el orden en que se devuelven las filas. Puede ordenar por una o más columnas, en orden ascendente o descendente, o por una expresión personalizada: ```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'); // ordenado por "creado +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDENADO POR "prioridad" DESC, "creado +$table->order('status = ? DESC', 'active'); // ORDER BY "status" = "active" DESC ``` -select() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Especifica las columnas que se devolverán de la base de datos. Por defecto, Nette Database Explorer sólo devuelve las columnas que se utilizan realmente en el código. Utilice el método `select()` cuando necesite recuperar expresiones específicas: + +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` -Ejemplos de uso: +Los alias definidos con `AS` son accesibles como propiedades del objeto `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; // acceder al alias +} ``` -limit() .[#toc-limit] ---------------------- +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Ejemplos de uso: +Limita el número de filas devueltas (LIMIT) y opcionalmente establece un offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (devuelve las 10 primeras filas) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Para la paginación, es más apropiado utilizar el método `page()`. -page() .[#toc-page] -------------------- -Una forma alternativa de establecer el límite y el desplazamiento: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Simplifica la paginación de los resultados. Acepta el número de página (empezando por 1) y el número de elementos por página. Opcionalmente, puede pasar una referencia a una variable donde se almacenará el número total de páginas: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Obteniendo el último número de página, pasado a la variable `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Agrupa las filas por las columnas especificadas (GROUP BY). Suele utilizarse en combinación con funciones de agregación: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Cuenta el número de productos de cada categoría +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Ejemplos de uso: +Establece una condición para filtrar filas agrupadas (HAVING). Puede utilizarse en combinación con el método `group()` y las funciones de agregado: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Encuentra categorías con más de 100 productos +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Lectura de datos +================ + +Para leer datos de la base de datos, existen varios métodos útiles: + +.[language-php] +| `foreach ($table as $key => $row)` | Recorre todas las filas, `$key` es el valor de la clave primaria, `$row` es un objeto ActiveRow +| `$row = $table->get($key)` | Devuelve una única fila por clave primaria. +| `$row = $table->fetch()` | Devuelve la fila actual y avanza el puntero a la siguiente +| `$array = $table->fetchPairs()` | Crea una matriz asociativa a partir de los resultados. +| `$array = $table->fetchAll()` | Devuelve todas las filas en forma de matriz +| `count($table)` | Devuelve el número de filas del objeto Selección. + +El objeto [ActiveRow |api:Nette\Database\Table\ActiveRow] es de sólo lectura. Esto significa que no puede cambiar los valores de sus propiedades. Esta restricción garantiza la coherencia de los datos y evita efectos secundarios inesperados. Los datos se obtienen de la base de datos, y cualquier cambio debe hacerse explícitamente y de forma controlada. + + +`foreach` - Iterar todas las filas +---------------------------------- -Ejemplos de uso: +La forma más sencilla de ejecutar una consulta y recuperar filas es iterando con el bucle `foreach`. Este ejecuta automáticamente la consulta SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = clave primaria, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtrado por otro valor de tabla .[#toc-joining-key] ----------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Ejecuta una consulta SQL y devuelve una fila por su clave primaria o `null` si no existe. + +```php +$book = $explorer->table('book')->get(123); // devuelve ActiveRow con ID 123 o null +if ($book) { + echo $book->title; +} +``` -A menudo es necesario filtrar los resultados por alguna condición que implique otra tabla de la base de datos. Este tipo de condiciones requieren la unión de tablas. Sin embargo, ya no es necesario escribirlas. -Supongamos que necesita obtener todos los libros cuyo autor se llame "Jon". Todo lo que tiene que escribir es la clave de unión de la relación y el nombre de la columna de la tabla unida. La clave de unión se deriva de la columna que hace referencia a la tabla que desea unir. En nuestro ejemplo (véase el esquema de la base de datos) se trata de la columna `author_id`, y basta con utilizar sólo la primera parte de la misma - `author` (el sufijo `_id` puede omitirse). `name` es una columna de la tabla `author` que queremos utilizar. Una condición para el traductor de libros (que está conectado por la columna `translator_id` ) se puede crear con la misma facilidad. +fetch(): ?ActiveRow .[method] +----------------------------- + +Devuelve una fila y avanza el puntero interno a la siguiente. Si no hay más filas, devuelve `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); +} ``` -La lógica de la clave de unión se rige por la implementación de [Conventions |api:Nette\Database\Conventions]. Le animamos a utilizar [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analiza sus claves externas y le permite trabajar fácilmente con estas relaciones. -La relación entre el libro y su autor es 1:N. La relación inversa también es posible. La llamamos **backjoin**. Veamos otro ejemplo. Queremos obtener todos los autores que han escrito más de 3 libros. Para hacer la unión inversa usamos la sentencia `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY`, también la condición tiene que ser escrita en forma de sentencia `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Devuelve los resultados como una matriz asociativa. El primer argumento especifica el nombre de la columna que se utilizará como clave en la matriz, y el segundo argumento especifica el nombre de la columna que se utilizará como valor: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book.id) > 3'); +$authors = $explorer->table('author')->fetchPairs('id', 'name'); +// [1 => 'Juan Pérez', 2 => 'Juana Pérez', ...] +``` + +Si sólo se especifica la columna clave, el valor será la fila completa, es decir, el objeto `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Puede que haya observado que la expresión de unión hace referencia al libro, pero no está claro si estamos uniendo a través de `author_id` o `translator_id`. En el ejemplo anterior, Selection une a través de la columna `author_id` porque se ha encontrado una coincidencia con la tabla de origen: la tabla `author`. Si no hubiera tal coincidencia y hubiera más posibilidades, Nette lanzaría [una AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Si se especifica `null` como clave, la matriz se indexará numéricamente empezando por cero: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'Juan Nadie', 1 => 'Juana Nadie', ...] +``` -Para realizar una unión a través de la columna `translator_id`, proporcione un parámetro opcional dentro de la expresión de unión. +También puede pasar una llamada de retorno como parámetro, que devolverá el propio valor o un par clave-valor para cada fila. Si la llamada de retorno sólo devuelve un valor, la clave será la clave principal de la fila: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Primer libro (Jan Novak)', ...] + +// La llamada de retorno también puede devolver un array con un par clave y valor: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Primer libro' => 'Jan Novak', ...] ``` -Veamos algunas expresiones de unión más difíciles. -Queremos encontrar todos los autores que han escrito algo sobre PHP. Todos los libros tienen etiquetas por lo que deberíamos seleccionar aquellos autores que hayan escrito algún libro con la etiqueta PHP. +fetchAll(): array .[method] +--------------------------- + +Devuelve todas las filas como una matriz asociativa de objetos `ActiveRow`, donde las claves son los valores de la clave principal. ```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, ...), ...] ``` -Consultas agregadas .[#toc-aggregate-queries] ---------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` Obtener número de filas -| `$table->count("DISTINCT $column")` Obtener el número de valores distintos -| `$table->min($column)` Obtener valor mínimo -| `$table->max($column)` Obtener el valor máximo -| `$table->sum($column)` Obtener la suma de todos los valores -| `$table->aggregation("GROUP_CONCAT($column)")` | Ejecutar cualquier función de agregación +El método `count()` sin parámetros devuelve el número de filas del objeto `Selection`: -.[caution] -El método `count()` sin ningún parámetro especificado selecciona todos los registros y devuelve el tamaño del array, lo cual es muy ineficiente. Por ejemplo, si necesita calcular el número de filas para la paginación, especifique siempre el primer argumento. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativa +``` + +Nota: `count()` con un parámetro realiza la función de agregación COUNT en la base de datos, como se describe a continuación. + + +ActiveRow::toArray(): array .[method] +------------------------------------- + +Convierte el objeto `ActiveRow` en una matriz asociativa donde las claves son los nombres de las columnas y los valores son los datos correspondientes. +```php +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray será ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Agregación .[#toc-aggregation] +============================== + +La clase `Selection` proporciona métodos para realizar fácilmente funciones de agregación (COUNT, SUM, MIN, MAX, AVG, etc.). + +.[language-php] +| `count($expr)` | Cuenta el número de filas. +| `min($expr)` | Devuelve el valor mínimo de una columna. +| `max($expr)` | Devuelve el valor máximo de una columna. +| `sum($expr)` | Devuelve la suma de los valores de una columna. +| `aggregation($function)` | Permite cualquier función de agregación, como `AVG()` o `GROUP_CONCAT()` + + +count(string $expr): int .[method] +---------------------------------- + +Ejecuta una consulta SQL con la función COUNT y devuelve el resultado. Este método se utiliza para determinar cuántas filas coinciden con una determinada condición: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `tabla`. +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `columna`) FROM `tabla`. +``` -Escapar y citar .[#toc-escaping-quoting] -======================================== +Nota: [count() |#count()] sin un parámetro simplemente devuelve el número de filas del objeto `Selection`. -Database Explorer es inteligente y escapa parámetros y comillas identificadores para usted. Sin embargo, es necesario seguir estas reglas básicas: -- las palabras clave, funciones y procedimientos deben ir en mayúsculas -- columnas y tablas deben ir en minúsculas -- pase variables como parámetros, no las concatene +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Los métodos `min()` y `max()` devuelven los valores mínimo y máximo de la columna o expresión especificada: + +```php +// SELECT MAX(`precio`) FROM `productos` WHERE `activo` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Devuelve la suma de los valores de la columna o expresión especificada: ```php -->where('name like ?', 'John'); // WRONG! generates: `name` `like` ? -->where('name LIKE ?', 'John'); // CORRECT +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` -->where('KEY = ?', $value); // WRONG! KEY is a keyword -->where('key = ?', $value); // CORRECT. generates: `key` = ? -->where('name = ' . $name); // WRONG! sql injection! -->where('name = ?', $name); // CORRECT +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // WRONG! pass variables as parameters, do not concatenate -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORRECT +Permite la ejecución de cualquier función de agregación. + +```php +// Calcula el precio medio de los productos de una categoría +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Combina las etiquetas de los productos en una sola cadena +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Si necesitamos agregar resultados que a su vez son resultado de una agregación y agrupación (por ejemplo, `SUM(value)` sobre filas agrupadas), especificamos la función de agregación que se aplicará a estos resultados intermedios como segundo argumento: + +```php +// Calcula el precio total de los productos en stock para cada categoría y, a continuación, suma estos precios. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); ``` -.[warning] -El uso incorrecto puede producir agujeros de seguridad +En este ejemplo, primero calculamos el precio total de los productos de cada categoría (`SUM(price * stock) AS category_total`) y agrupamos los resultados por `category_id`. A continuación, utilizamos `aggregation('SUM(category_total)', 'SUM')` para sumar estos subtotales. El segundo argumento `'SUM'` especifica la función de agregación que se aplicará a los resultados intermedios. -Obtención de datos .[#toc-fetching-data] -======================================== +Insertar, actualizar y eliminar .[#toc-insert-update-delete] +============================================================ -| `foreach ($table as $id => $row)` | Iterar sobre todas las filas del resultado -| `$row = $table->get($id)` Obtener una fila con ID $id de la tabla -| `$row = $table->fetch()` Obtener la siguiente fila del resultado -| `$array = $table->fetchPairs($key, $value)` Obtener todos los valores de la matriz asociativa -| `$array = $table->fetchPairs($value)` Obtener todas las filas de la matriz asociativa -| `count($table)` Obtener el número de filas del conjunto de resultados +Nette Database Explorer simplifica la inserción, actualización y eliminación de datos. Todos los métodos mencionados lanzan un `Nette\Database\DriverException` en caso de error. -Insertar, Actualizar y Borrar .[#toc-insert-update-delete] -========================================================== +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- -El método `insert()` acepta un array de objetos Traversable (por ejemplo [ArrayHash |utils:arrays#ArrayHash] que devuelve [formularios |forms:]): +Inserta nuevos registros en una tabla. + +**Insertar un solo registro:** + +El nuevo registro se pasa como un array asociativo u objeto iterable (como `ArrayHash` utilizado en [formularios |forms:]), donde las claves coinciden con los nombres de las columnas de la tabla. + +Si la tabla tiene una clave primaria definida, el método devuelve un objeto `ActiveRow`, que se vuelve a cargar desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (por ejemplo, disparadores, valores de columna por defecto o cálculos de autoincremento). Esto garantiza la coherencia de los datos, y el objeto siempre contiene los datos actuales de la base de datos. Si no se define explícitamente una clave primaria, el método devuelve los datos de entrada como una matriz. ```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 es una instancia de ActiveRow que contiene los datos completos de la fila insertada, +// incluyendo el ID autogenerado y cualquier cambio realizado por triggers +echo $row->id; // Muestra el ID del nuevo usuario insertado +echo $row->created_at; // Indica la hora de creación si ha sido establecida por un activador ``` -Si la clave primaria está definida en la tabla, se devuelve un objeto ActiveRow que contiene la fila insertada. +**Insertar varios registros a la vez:** -Inserción múltiple: +El método `insert()` permite insertar varios registros con una sola consulta SQL. En este caso, devuelve el número de filas insertadas. ```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 será 2 ``` -Se pueden pasar como parámetros ficheros u objetos DateTime: +También puede pasar como parámetro un objeto `Selection` con una selección de datos. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Inserción de valores especiales:** + +Los valores pueden incluir archivos, objetos `DateTime` o literales SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // convierte al formato de la base de datos + 'avatar' => fopen('image.jpg', 'rb'), // inserta el contenido del archivo binario + 'uuid' => $explorer::literal('UUID()'), // llama a la función UUID() ]); ``` -Actualización (devuelve el recuento de filas afectadas): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Actualiza las filas de una tabla basándose en un filtro especificado. Devuelve el número de filas realmente modificadas. + +Las columnas a actualizar se pasan como un array asociativo u objeto iterable (como `ArrayHash` utilizado en [formularios |forms:]), donde las claves coinciden con los nombres de las columnas de la tabla: ```php -$count = $explorer->table('users') - ->where('id', 10) // must be called before 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 ``` -Para actualizar podemos utilizar los operadores `+=` a `-=`: +Para modificar valores numéricos, puede utilizar los operadores `+=` y `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // aumenta el valor de la columna "puntos" en 1 + 'coins-=' => 1, // disminuye el valor de la columna 'coins' en 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Borrado (devuelve el recuento de filas borradas): + +Selection::delete(): int .[method] +---------------------------------- + +Elimina filas de una tabla basándose en un filtro especificado. Devuelve el número de filas eliminadas. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Cuando llame a `update()` o `delete()`, asegúrese de utilizar `where()` para especificar las filas que se van a actualizar o eliminar. Si no se utiliza `where()`, la operación se realizará en toda la tabla. + -Trabajar con relaciones .[#toc-working-with-relationships] -========================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Actualiza los datos de una fila de la base de datos representada por el objeto `ActiveRow`. Acepta datos iterables como parámetro, donde las claves son nombres de columnas. Para modificar valores numéricos, puede utilizar los operadores `+=` y `-=`: -Tiene Una Relación .[#toc-has-one-relation] -------------------------------------------- -Tiene una relación es un caso de uso común. Libro *tiene un* autor. Libro *tiene un* traductor. La obtención de una fila relacionada se realiza principalmente mediante el método `ref()`. Acepta dos argumentos: nombre de la tabla de destino y columna de unión de origen. Véase el ejemplo: +Una vez realizada la actualización, el `ActiveRow` se vuelve a cargar automáticamente desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (por ejemplo, triggers). El método devuelve `true` sólo si se ha producido un cambio real en los datos. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // incrementa el número de visitas +]); +echo $article->views; // Muestra el recuento de visitas actual ``` -En el ejemplo anterior obtenemos la entrada de autor relacionada de la tabla `author`, la clave primaria de autor se busca en la columna `book.author_id`. El método Ref() devuelve una instancia de ActiveRow o null si no hay ninguna entrada apropiada. La fila devuelta es una instancia de ActiveRow, por lo que podemos trabajar con ella del mismo modo que con la entrada del libro. +Este método sólo actualiza una fila específica de la base de datos. Para actualizaciones masivas de varias filas, utilice el método [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Elimina una fila de la base de datos representada por el objeto `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Borra el libro con ID 1 +``` + +Este método borra sólo una fila específica de la base de datos. Para el borrado masivo de múltiples filas, utilice el método [Selection::delete() |#Selection::delete()]. -// or directly -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +Relaciones entre tablas .[#toc-relationships-between-tables] +============================================================ + +En las bases de datos relacionales, los datos se dividen en varias tablas y se conectan a través de claves externas. Nette Database Explorer ofrece una forma revolucionaria de trabajar con estas relaciones - sin escribir consultas JOIN ni requerir ninguna configuración o generación de entidades. + +Para la demostración, utilizaremos el **ejemplo de base de datos**[(disponible en GitHub |https://github.com/nette-examples/books]). La base de datos incluye las siguientes tablas: + +- `author` - autores y traductores (columnas `id`, `name`, `web`, `born`) +- `book` - libros (columnas `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - etiquetas (columnas `id`, `name`) +- `book_tag` - tabla de enlaces entre libros y etiquetas (columnas `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Estructura de la base de datos .<> + +En este ejemplo de base de datos de libros, encontramos varios tipos de relaciones (simplificadas respecto a la realidad): + +- **Uno-a-muchos (1:N)** - Cada libro **tiene un** autor; un autor puede escribir **múltiples** libros. +- De cero a muchos (0:N)** - Un libro **puede tener** un traductor; un traductor puede traducir **múltiples** libros. +- Cero a uno (0:1)** - Un libro puede tener una secuela. +- De muchos a muchos (M:N)** - Un libro **puede tener varias** etiquetas, y una etiqueta puede asignarse a **varios** libros. + +En estas relaciones, siempre hay una **tabla padre** y una **tabla hijo**. Por ejemplo, en la relación entre autores y libros, la tabla `author` es el padre, y la tabla `book` es el hijo - se puede pensar que un libro siempre "pertenece" a un autor. Esto también se refleja en la estructura de la base de datos: la tabla hija `book` contiene la clave foránea `author_id`, que hace referencia a la tabla padre `author`. + +Si queremos mostrar los libros junto con los nombres de sus autores, tenemos dos opciones. O bien recuperamos los datos mediante una única consulta SQL con un JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +O bien recuperamos los datos en dos pasos -primero los libros, luego sus autores- y los ensamblamos en PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Book también tiene un traductor, por lo que obtener el nombre del traductor es bastante fácil. +El segundo enfoque es, sorprendentemente, **más eficiente**. Los datos se obtienen una sola vez y pueden utilizarse mejor en caché. Así es exactamente como funciona Nette Database Explorer - se encarga de todo bajo el capó y le proporciona una API limpia: + ```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 es un registro de la tabla 'author + echo 'translated by: ' . $book->translator?->name; +} ``` -Todo esto está muy bien, pero es algo engorroso, ¿no cree? Database Explorer ya contiene las definiciones de las claves foráneas, así que ¿por qué no utilizarlas automáticamente? Hagámoslo. -Si llamamos a una propiedad que no existe, ActiveRow intenta resolver el nombre de la propiedad llamante como relación 'tiene una'. Obtener esta propiedad es lo mismo que llamar al método ref() con un solo argumento. Llamaremos **clave** al único argumento. La clave se resolverá como una relación particular de clave externa. La clave pasada se compara con las columnas de la fila, y si coincide, la clave foránea definida en la columna coincidente se utiliza para obtener datos de la tabla de destino relacionada. Véase el ejemplo: +Acceso a la tabla principal .[#toc-accessing-the-parent-table] +-------------------------------------------------------------- + +Acceder a la tabla padre es sencillo. Se trata de relaciones del tipo *un libro tiene un autor* o *un libro puede tener un traductor*. Se puede acceder al registro relacionado a través de la propiedad del objeto `ActiveRow` - el nombre de la propiedad coincide con el nombre de la columna de la clave ajena sin el sufijo `id`: ```php -$book->author->name; -// same as -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // busca al autor mediante la columna "author_id +echo $book->translator?->name; // busca al traductor mediante la columna "translator_id ``` -La instancia ActiveRow no tiene columna de autor. Se buscan todas las columnas de libros que coincidan con *key*. En este caso, la coincidencia significa que el nombre de la columna debe contener la clave. Así, en el ejemplo anterior, la columna `author_id` contiene la cadena "author" y, por lo tanto, coincide con la clave "author". Si desea obtener el traductor del libro, sólo tiene que utilizar, por ejemplo, "traductor" como clave, porque la clave "traductor" coincidirá con la columna `translator_id`. Encontrará más información sobre la lógica de correspondencia de claves en el capítulo [Expresiones de unión |#joining-key]. +Al acceder a la propiedad `$book->author`, Explorer busca una columna en la tabla `book` que contenga la cadena `author` (es decir, `author_id`). En función del valor de esta columna, recupera el registro correspondiente de la tabla `author` y lo devuelve como un objeto `ActiveRow`. Del mismo modo, `$book->translator` utiliza la columna `translator_id`. Dado que la columna `translator_id` puede contener `null`, se utiliza el operador `?->`. + +Un método alternativo es `ref()`, que acepta dos argumentos (el nombre de la tabla de destino y la columna de enlace) y devuelve una instancia de `ActiveRow` o `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; // enlace al autor +echo $book->ref('author', 'translator_id')->name; // enlace al traductor ``` -Si desea buscar varios libros, utilice el mismo método. Nette Database Explorer obtendrá los autores y traductores de todos los libros obtenidos a la vez. +El método `ref()` es útil si no se puede utilizar el acceso basado en propiedades, por ejemplo, cuando la tabla contiene una columna con el mismo nombre que la propiedad (`author`). En otros casos, se recomienda utilizar el acceso basado en propiedades para mejorar la legibilidad. + +Explorer optimiza automáticamente las consultas a la base de datos. Al iterar por los libros y acceder a sus registros relacionados (autores, traductores), Explorer no genera una consulta para cada libro individualmente. En su lugar, ejecuta sólo **una consulta SELECT para cada tipo de relación**, reduciendo significativamente la carga de la base de datos. Por ejemplo: ```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; } ``` -El código sólo ejecutará estas 3 consultas: +Este código ejecutará sólo tres consultas optimizadas a la base de datos: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +La lógica para identificar la columna de enlace está definida por la implementación de [Conventions |api:Nette\Database\Conventions]. Recomendamos utilizar [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analiza las claves externas y permite trabajar sin problemas con las relaciones de tablas existentes. + -Tiene mucha relación .[#toc-has-many-relation] ----------------------------------------------- +Acceso a la tabla hija .[#toc-accessing-the-child-table] +-------------------------------------------------------- -La relación "tiene muchos" es la relación inversa a "tiene uno". El autor *ha* escrito *muchos* libros. El autor *ha* traducido *muchos* libros. Como puedes ver, este tipo de relación es un poco más difícil porque la relación tiene nombre ('escrito', 'traducido'). La instancia ActiveRow tiene el método `related()`, que devolverá un array de entradas relacionadas. Las entradas también son instancias de ActiveRow. Véase el ejemplo siguiente: +El acceso a la tabla hija funciona en sentido inverso. Ahora preguntamos *qué libros escribió este autor* o *qué libros tradujo este traductor*. Para este tipo de consulta, utilizamos el método `related()`, que devuelve un objeto `Selection` con registros relacionados. He aquí un ejemplo: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Muestra todos los libros escritos por el autor foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Salidas de todos los libros traducidos por el autor foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Método `related()` El método acepta la descripción completa de la unión pasada como dos argumentos o como un argumento unido por un punto. El primer argumento es la tabla de destino, el segundo es la columna de destino. +El método `related()` acepta la descripción de la relación como argumento único utilizando la notación de puntos o como dos argumentos separados: + +```php +$author->related('book.translator_id'); // un solo argumento +$author->related('book', 'translator_id'); // dos argumentos +``` + +Explorer puede detectar automáticamente la columna de vinculación correcta basándose en el nombre de la tabla padre. En este caso, enlaza a través de la columna `book.author_id` porque el nombre de la tabla origen es `author`: ```php -$author->related('book.translator_id'); -// same as -$author->related('book', 'translator_id'); +$author->related('book'); // usa book.author_id ``` -Puede utilizar la heurística de Nette Database Explorer basada en claves externas y proporcionar sólo el argumento **clave**. La clave se comparará con todas las claves externas que apunten a la tabla actual (`author` table). Si hay una coincidencia, Nette Database Explorer usará esta clave foránea, de lo contrario lanzará una [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] o [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Puede encontrar más información sobre la lógica de coincidencia de claves en el capítulo [Expresiones de unión |#joining-key]. +Si existen múltiples conexiones posibles, Explorer lanzará una excepción [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Por supuesto, puede llamar a métodos relacionados para todos los autores obtenidos, Nette Database Explorer obtendrá de nuevo los libros apropiados a la vez. +Por supuesto, también podemos utilizar el método `related()` cuando iteramos a través de múltiples registros en un bucle, y Explorer optimizará automáticamente las consultas también en este caso: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -El ejemplo anterior sólo ejecutará dos consultas: +Este código genera sólo dos consultas SQL eficientes: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Creación manual del explorador .[#toc-creating-explorer-manually] -================================================================= +Relación múltiple .[#toc-many-to-many-relationship] +--------------------------------------------------- + +Para una relación de muchos a muchos (M:N), se requiere una **tabla de unión** (en nuestro caso, `book_tag`). Esta tabla contiene dos columnas de clave externa (`book_id`, `tag_id`). Cada columna hace referencia a la clave primaria de una de las tablas conectadas. Para recuperar los datos relacionados, primero obtenemos los registros de la tabla de enlace mediante `related('book_tag')`, y luego continuamos con los datos de destino: + +```php +$book = $explorer->table('book')->get(1); +// Muestra los nombres de las etiquetas asignadas al libro +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // obtiene el nombre de la etiqueta a través de la tabla de enlaces +} + +$tag = $explorer->table('tag')->get(1); +// Dirección opuesta: muestra los títulos de los libros con esta etiqueta +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // busca el título del libro +} +``` + +De nuevo, Explorer optimiza las consultas SQL de forma eficiente: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Consulta a través de tablas relacionadas .[#toc-querying-through-related-tables] +-------------------------------------------------------------------------------- + +En los métodos `where()`, `select()`, `order()`, y `group()`, puede utilizar notaciones especiales para acceder a columnas de otras tablas. Explorer crea automáticamente los JOINs necesarios. + +**La notación de puntos** (`parent_table.column`) se utiliza para relaciones 1:N vistas desde la perspectiva de la tabla padre: + +```php +$books = $explorer->table('book'); + +// Busca libros cuyo nombre de autor empiece por "Jon". +$books->where('author.name LIKE ?', 'Jon%'); + +// Ordena los libros por nombre de autor de forma descendente +$books->order('author.name DESC'); + +// Muestra el título del libro y el nombre del autor +$books->select('book.title, author.name'); +``` + +**La notación de puntos** se utiliza para las relaciones 1:N desde la perspectiva de la tabla padre: + +```php +$authors = $explorer->table('author'); + +// Busca autores que hayan escrito un libro con "PHP" en el título +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Cuenta el número de libros de cada autor +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +En el ejemplo anterior con notación de dos puntos (`:book.title`), la columna de clave externa no se especifica explícitamente. Explorer detecta automáticamente la columna correcta basándose en el nombre de la tabla padre. En este caso, se une a través de la columna `book.author_id` porque el nombre de la tabla origen es `author`. Si existen múltiples conexiones posibles, Explorer lanza la excepción [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +La columna de enlace se puede especificar explícitamente entre paréntesis: + +```php +// Busca autores que hayan traducido un libro cuyo título incluya "PHP". +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Las notaciones pueden encadenarse para acceder a datos de varias tablas: + +```php +// Encuentra autores de libros etiquetados con "PHP". +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + -Se puede crear una conexión a la base de datos utilizando la configuración de la aplicación. En estos casos se crea un servicio `Nette\Database\Explorer` que puede pasarse como dependencia utilizando el contenedor DI. +Ampliación de las condiciones para JOIN .[#toc-extending-conditions-for-join] +----------------------------------------------------------------------------- -Sin embargo, si Nette Database Explorer se utiliza como herramienta independiente, es necesario crear manualmente una instancia del objeto `Nette\Database\Explorer`. +El método `joinWhere()` añade condiciones adicionales a las uniones de tablas en SQL después de la palabra clave `ON`. + +Por ejemplo, supongamos que queremos encontrar libros traducidos por un traductor concreto: + +```php +// Busca libros traducidos por un traductor llamado 'David +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN autor traductor ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +En la condición `joinWhere()`, puede utilizar las mismas construcciones que en el método `where()`: operadores, marcadores de posición, matrices de valores o expresiones SQL. + +Para consultas más complejas con múltiples JOINs, se pueden definir alias de tablas: ```php -// $storage implements Nette\Caching\Storage: -$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); +$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) +``` + +Tenga en cuenta que mientras que el método `where()` añade condiciones a la cláusula `WHERE`, el método `joinWhere()` amplía las condiciones de la cláusula `ON` durante las uniones de tablas. + + +Creación manual del explorador .[#toc-manually-creating-explorer] +================================================================= + +Si no está utilizando el contenedor Nette DI, puede crear una instancia de `Nette\Database\Explorer` manualmente: + +```php +use Nette\Database; + +// $storage implementa Nette\Caching\Storage, por ejemplo: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// conexión a base de datos +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// gestiona el reflejo de la estructura de la base de datos +$structure = new Database\Structure($connection, $storage); +// define reglas para la asignación de nombres de tablas, columnas y claves externas +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/es/security.texy b/database/es/security.texy new file mode 100644 index 0000000000..2e59c93436 --- /dev/null +++ b/database/es/security.texy @@ -0,0 +1,145 @@ +Riesgos de seguridad +******************** + +.[perex] +Las bases de datos contienen a menudo datos sensibles y permiten operaciones peligrosas. Nette Database proporciona una serie de características de seguridad. Sin embargo, es crucial entender la diferencia entre API seguras y no seguras. + + +Inyección SQL .[#toc-sql-injection] +=================================== + +La inyección SQL es el riesgo de seguridad más grave cuando se trabaja con bases de datos. Se produce cuando una entrada de usuario no verificada pasa a formar parte de una consulta SQL. Un atacante puede inyectar sus propios comandos SQL, obteniendo o modificando datos en la base de datos. + +```php +// ❌ CÓDIGO INSEGURO - vulnerable a la inyección SQL. +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// El atacante puede introducir algo como ' OR '1'='1 +// La consulta resultante será: +// SELECT * FROM usuarios WHERE nombre = '' OR '1'='1' +// Esto devuelve todos los usuarios. +``` + +Lo mismo ocurre con Database Explorer: + +```php +// ❌ CÓDIGO DE SEGURIDAD +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Consultas parametrizadas seguras .[#toc-safe-parameterized-queries] +=================================================================== + +La forma segura de insertar valores en consultas SQL es a través de consultas parametrizadas. Nette Database proporciona varias formas de utilizarlas. + + +Signos de interrogación .[#toc-placeholder-question-marks] +---------------------------------------------------------- + +El método más sencillo consiste en utilizar signos de interrogación: + +```php +// ✅ Consultas parametrizadas seguras. +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Condición segura en el Explorador +$table->where('name = ?', $_GET['name']); +``` + +Lo mismo se aplica a todos los demás métodos de Database Explorer que permiten insertar expresiones con signos de interrogación de marcador de posición y parámetros. + +.[warning] +Los valores deben ser de tipo escalar (`string`, `int`, `float`, `bool`) o `null`. Si, por ejemplo, `$_GET['name']` es una matriz, Nette Database incluirá todos sus elementos en la consulta SQL, lo que puede resultar indeseable. + + +Arrays de valores .[#toc-value-arrays] +-------------------------------------- + +Para las cláusulas `INSERT`, `UPDATE`, o `WHERE`, podemos utilizar matrices de valores: + +```php +// ✅ Safe INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ UPDATE seguro +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database escapa automáticamente todos los valores pasados a través de consultas parametrizadas. Sin embargo, debemos asegurarnos de que el tipo de datos de los parámetros es correcto. + + +Las claves de array no son una API segura .[#toc-array-keys-are-not-a-safe-api] +=============================================================================== + +Mientras que los valores de las matrices son seguros, no puede decirse lo mismo de las claves: + +```php +// ❌ CÓDIGO INSEGURO - las claves pueden contener inyección SQL. +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Para los comandos `INSERT` y `UPDATE`, se trata de un fallo de seguridad crítico: un atacante podría insertar o modificar cualquier columna de la base de datos. Por ejemplo, podrían establecer `is_admin = 1` o insertar datos arbitrarios en columnas sensibles. + +En condiciones `WHERE`, esto es aún más peligroso porque permite la **enumeración SQL** - una técnica para recuperar gradualmente información sobre la base de datos. Un atacante podría intentar explorar los salarios de los empleados inyectando en `$_GET` de esta manera: + +```php +$_GET = ['salary >', 100000]; // empieza a determinar los rangos salariales +``` + +El principal problema, sin embargo, es que las condiciones de `WHERE` admiten expresiones SQL en las claves: + +```php +// Uso legítimo de operadores en claves +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ INSEGURO: el atacante puede inyectar su propio SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // permite al atacante obtener salarios de administrador +``` + +Esto es una vez más **inyección SQL**. + + +Lista blanca de columnas .[#toc-whitelisting-columns] +----------------------------------------------------- + +Si desea permitir a los usuarios elegir columnas, utilice siempre una lista blanca: + +```php +// ✅ Tratamiento seguro - sólo columnas permitidas +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Identificadores dinámicos .[#toc-dynamic-identifiers] +===================================================== + +Para los nombres dinámicos de tablas y columnas, utilice el marcador de posición `?name`: + +```php +// ✅ Uso seguro de identificadores de confianza +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ INSEGURO: no utilizar nunca la entrada del usuario. +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +El símbolo `?name` sólo debe utilizarse para valores de confianza definidos en el código de la aplicación. Para los valores proporcionados por el usuario, vuelva a utilizar una lista blanca. 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 diff --git a/database/fr/@left-menu.texy b/database/fr/@left-menu.texy index ced579ea71..99265e6b8e 100644 --- a/database/fr/@left-menu.texy +++ b/database/fr/@left-menu.texy @@ -4,3 +4,4 @@ Base de données - [Explorer] - [Réflexion |Reflection] - [Configuration] +- [Risques pour la sécurité |security] diff --git a/database/fr/explorer.texy b/database/fr/explorer.texy index 3ceb3d9bb2..a08f3347d8 100644 --- a/database/fr/explorer.texy +++ b/database/fr/explorer.texy @@ -3,548 +3,927 @@ Explorateur de bases de données
-Nette Database Explorer simplifie considérablement l'extraction de données de la base de données sans avoir à écrire de requêtes SQL. +Nette Database Explorer est une couche puissante qui simplifie considérablement l'extraction de données de la base de données sans qu'il soit nécessaire d'écrire des requêtes SQL. -- utilise des requêtes efficaces -- aucune donnée n'est transmise inutilement -- présente une syntaxe élégante +- Travailler avec des données est naturel et facile à comprendre +- Génère des requêtes SQL optimisées qui ne récupèrent que les données nécessaires. +- Fournit un accès facile aux données connexes sans qu'il soit nécessaire d'écrire des requêtes JOIN +- Fonctionne immédiatement sans configuration ni génération d'entités
-Pour utiliser Database Explorer, commencez par une table - appelez `table()` sur un objet [api:Nette\Database\Explorer]. La manière la plus simple d'obtenir une instance d'objet de contexte est [décrite ici |core#Connection and Configuration], ou, pour le cas où Nette Database Explorer est utilisé comme un outil autonome, elle peut être [créée manuellement |#Creating Explorer Manually]. +Nette Database Explorer est une extension de la couche de bas niveau [Nette Database Core |core], qui ajoute une approche orientée objet pratique à la gestion des bases de données. + +Pour travailler avec Explorer, il suffit d'appeler la méthode `table()` sur l'objet [api:Nette\Database\Explorer] (la manière de l'obtenir est [décrite ici |core#Connection and Configuration]) : ```php -$books = $explorer->table('book'); // le nom de la table de la base de données est 'book'. +$books = $explorer->table('book'); // 'book' est le nom de la table ``` -L'appel renvoie une instance de l'objet [Selection |api:Nette\Database\Table\Selection], qui peut être itéré pour récupérer tous les livres. Chaque élément (une ligne) est représenté par une instance de [ActiveRow |api:Nette\Database\Table\ActiveRow] avec des données mappées à ses propriétés : +La méthode renvoie un objet [Selection |api:Nette\Database\Table\Selection], qui représente une requête SQL. D'autres méthodes peuvent être enchaînées à cet objet pour filtrer et trier les résultats. La requête est assemblée et exécutée uniquement lorsque les données sont demandées, par exemple en itérant avec `foreach`. Chaque ligne est représentée par un objet [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // produit la colonne "title" (titre) + echo $book->author_id; // sort la colonne "author_id". } ``` -L'obtention d'une ligne spécifique se fait par la méthode `get()`, qui renvoie directement une instance ActiveRow. +Explorer simplifie grandement l'utilisation des [relations entre les tables |#Vazby mezi tabulkami]. L'exemple suivant montre comment nous pouvons facilement produire des données à partir de tables liées (les livres et leurs auteurs). Notez qu'il n'est pas nécessaire d'écrire des requêtes JOIN ; Nette les génère pour nous : ```php -$book = $explorer->table('book')->get(2); // renvoie le livre avec l'id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // crée une jointure avec la table "auteur". +} ``` -Voyons un cas d'utilisation courant. Vous avez besoin de récupérer des livres et leurs auteurs. Il s'agit d'une relation 1:N courante. La solution la plus courante est de récupérer les données en utilisant une seule requête SQL avec des jointures de tables. La deuxième possibilité est de récupérer les données séparément, d'exécuter une requête pour obtenir les livres, puis d'obtenir un auteur pour chaque livre au moyen d'une autre requête (par exemple, dans votre cycle foreach). Cela pourrait être facilement optimisé pour n'exécuter que deux requêtes, une pour les livres, et une autre pour les auteurs nécessaires - et c'est exactement la façon dont Nette Database Explorer le fait. +Nette Database Explorer optimise les requêtes pour une efficacité maximale. L'exemple ci-dessus n'effectue que deux requêtes SELECT, que nous traitions 10 ou 10 000 livres. -Dans les exemples ci-dessous, nous allons travailler avec le schéma de base de données de la figure. Il existe des liens OneHasMany (1:N) (auteur du livre `author_id` et traducteur éventuel `translator_id`, qui peut être `null`) et ManyHasMany (M:N) entre le livre et ses étiquettes. +En outre, Explorer repère les colonnes utilisées dans le code et ne récupère que celles-ci dans la base de données, ce qui permet d'améliorer encore les performances. Ce comportement est entièrement automatique et adaptatif. Si vous modifiez ultérieurement le code pour utiliser des colonnes supplémentaires, Explorer ajuste automatiquement les requêtes. Vous n'avez pas besoin de configurer quoi que ce soit ou de réfléchir aux colonnes qui seront nécessaires - laissez cela à Nette. -[Un exemple, incluant un schéma, se trouve sur GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Structure de la base de données utilisée dans les exemples .<> +Filtrage et tri .[#toc-filtering-and-sorting] +============================================= -Le code suivant liste le nom de l'auteur pour chaque livre et toutes ses balises. Nous [verrons dans un instant |#Working with relationships] comment cela fonctionne en interne. +La classe `Selection` propose des méthodes pour filtrer et trier les données. -```php -$books = $explorer->table('book'); +.[language-php] +| La classe `where($condition, ...$params)` | Ajoute une condition WHERE. Les conditions multiples sont combinées à l'aide de AND. +| `whereOr(array $conditions)` | Ajoute un groupe de conditions WHERE combinées à l'aide de OR. +| `wherePrimary($value)` | Ajoute une condition WHERE basée sur la clé primaire. +| `order($columns, ...$params)` | Définit le tri à l'aide de ORDER BY +| `select($columns, ...$params)` | Spécifie les colonnes à extraire +| `limit($limit, $offset = null)` | Limite le nombre de lignes (LIMIT) et définit optionnellement OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Définit la pagination +| `group($columns, ...$params)` | Regroupe les lignes (GROUP BY) | +| `having($condition, ...$params)`| Ajoute une condition HAVING pour le filtrage des lignes groupées. -foreach ($books as $book) { - echo 'titre: ' . $book->title; - echo 'écrit par: ' . $book->author->name; // $book->author est une ligne de la table 'author'. +Les méthodes peuvent être enchaînées ( [interface fluide |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 est une ligne de la table 'tag'. - } -} -``` +Ces méthodes permettent également d'utiliser des notations spéciales pour accéder aux [données de tables connexes |#Dotazování přes související tabulky]. -Vous serez heureux de constater l'efficacité du fonctionnement de la couche de base de données. L'exemple ci-dessus fait un nombre constant de requêtes qui ressemblent à ceci : -```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)) -``` +Échappatoires et identificateurs .[#toc-escaping-and-identifiers] +----------------------------------------------------------------- -Si vous utilisez le [cache |caching:] (activé par défaut), aucune colonne ne sera interrogée inutilement. Après la première requête, le cache stockera les noms des colonnes utilisées et Nette Database Explorer exécutera les requêtes uniquement avec les colonnes nécessaires : +Les méthodes échappent automatiquement les paramètres et les identificateurs de citation (noms de tables et de colonnes), ce qui permet d'éviter les injections SQL. Pour garantir un fonctionnement correct, quelques règles doivent être respectées : -```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)) +- Écrire les mots-clés, les noms de fonctions, les procédures, etc. en **majuscules**. +- Écrire les noms de colonnes et de tables en **minuscules**. +- Toujours passer des chaînes de caractères en utilisant des **paramètres**. + +```php +where('name = ' . $name); // **DISASTER**: vulnérable à l'injection SQL +where('name LIKE "%search%"'); // **WRONG**: complique les citations automatiques +where('name LIKE ?', '%search%'); // **CORRECT**: valeur passée en paramètre + +where('name like ?', $name); // **WRONG**: génère: `nom` `like` ? +where('name LIKE ?', $name); // **CORRECT**: génère: `nom` LIKE ? `nom` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`nom`) = ? ``` -Sélections .[#toc-selections] -============================= +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Voir les possibilités de filtrer et de restreindre les lignes [api:Nette\Database\Table\Selection]: +Filtre les résultats à l'aide de conditions WHERE. Sa force réside dans la gestion intelligente de différents types de valeurs et dans la sélection automatique d'opérateurs SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Définir WHERE en utilisant AND comme colle si deux conditions ou plus sont fournies. -| `$table->whereOr($where)` | Définir WHERE en utilisant OR comme colle si deux ou plusieurs conditions sont fournies -| `$table->order($columns)` | Définir ORDER BY, peut être une expression `('column DESC, id DESC')` -| `$table->select($columns)` | Définir les colonnes extraites, peut être une expression `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Définir LIMIT et OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Activation de la pagination -| `$table->group($columns)` | Définir GROUP BY -| `$table->having($having)` | Définir HAVING +Utilisation de base : -Nous pouvons utiliser ce que l'on appelle une [interface fluide |nette:introduction-to-object-oriented-programming#fluent-interfaces], par exemple `$table->where(...)->order(...)->limit(...)`. Plusieurs conditions `where` ou `whereOr` sont liées par l'opérateur `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' +``` +Grâce à la détection automatique des opérateurs adéquats, vous n'avez pas besoin de gérer les cas particuliers, Nette s'en charge pour vous : -où() .[#toc-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) +// L'espace réservé ? peut être utilisé sans opérateur: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer peut ajouter automatiquement les opérateurs nécessaires pour les valeurs passées : +La méthode traite également correctement les conditions négatives et les tableaux vides : -.[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])` | champ IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OR name = 'Jon Snow' (nom de l'utilisateur) -| `$table->where('field', $explorer->table($tableName))` | champ IN (SELECT $primary FROM $tableName) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | champ IN (SELECT col FROM $tableName) +```php +$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- ne trouve rien +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- Trouve tout +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- trouve tout +// $table->where('NOT id ?', $ids); // ATTENTION: Cette syntaxe n'est pas supportée. +``` -Vous pouvez fournir un caractère de remplacement même sans opérateur de colonne. Ces appels sont les mêmes. +Vous pouvez également passer le résultat d'une autre requête de table en tant que paramètre, créant ainsi une sous-requête : ```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')); ``` -Cette fonctionnalité permet de générer l'opérateur correct en fonction de la valeur : +Les conditions peuvent également être transmises sous la forme d'un tableau dont les éléments sont combinés à l'aide de l'opérateur AND : ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`prix_final` < `prix_original`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -La sélection gère correctement les conditions négatives, et fonctionne également pour les tableaux vides : +Dans le tableau, des paires clé-valeur peuvent être utilisées, et Nette choisira automatiquement les opérateurs corrects : ```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], +]); +``` -// ceci lancera une exception, cette syntaxe n'est pas supportée. -$table->where('NOT id ?', $ids); +Nous pouvons également mélanger des expressions SQL avec des espaces réservés et des paramètres multiples. Ceci est utile pour les conditions complexes avec des opérateurs définis avec précision : + +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // deux paramètres sont passés sous forme de tableau +]); ``` +Plusieurs appels à `where()` combinent automatiquement les conditions à l'aide de AND. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Exemple d'utilisation sans paramètres : +Similaire à `where()`, mais combine les conditions à l'aide de 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, ]); ``` -On utilise les paramètres. Si vous ne spécifiez pas d'opérateur, Nette Database Explorer ajoutera automatiquement l'opérateur approprié : +Des expressions plus complexes peuvent également être utilisées : ```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, ]); ``` -La clé peut contenir une expression contenant des points d'interrogation joker, puis passer des paramètres dans la valeur : + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Ajoute une condition pour la clé primaire de la table : ```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]); ``` +Si la table a une clé primaire composite (par exemple, `foo_id`, `bar_id`), nous la transmettons sous forme de tableau : + +```php +// WHERE `foo_id` = 1 AND `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); -commande() .[#toc-order] ------------------------- +// 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(); +``` -Exemples d'utilisation : + +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Spécifie l'ordre dans lequel les lignes sont renvoyées. Vous pouvez trier par une ou plusieurs colonnes, par ordre croissant ou décroissant, ou par une expression personnalisée : ```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 `créé` +$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() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Spécifie les colonnes à renvoyer de la base de données. Par défaut, Nette Database Explorer ne renvoie que les colonnes réellement utilisées dans le code. Utilisez la méthode `select()` lorsque vous devez récupérer des expressions spécifiques : -Exemples d'utilisation : +```php +// SELECT *, DATE_FORMAT(`créé_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` + +Les alias définis à l'aide de `AS` sont alors accessibles en tant que propriétés de l'objet `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; // accéder à l'alias +} ``` -limit() .[#toc-limit] ---------------------- +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Exemples d'utilisation : +Limite le nombre de lignes retournées (LIMIT) et définit éventuellement un offset : ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (renvoie les 10 premières lignes) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Pour la pagination, il est plus approprié d'utiliser la méthode `page()`. -page() .[#toc-page] -------------------- -Une autre façon de définir la limite et le décalage : +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Simplifie la pagination des résultats. Il accepte le numéro de page (à partir de 1) et le nombre d'éléments par page. En option, vous pouvez passer une référence à une variable dans laquelle le nombre total de pages sera stocké : ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Récupérer le dernier numéro de page, passé à la variable `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Regroupe les lignes en fonction des colonnes spécifiées (GROUP BY). Il est généralement utilisé en combinaison avec des fonctions d'agrégation : ```php -$table->page($page, $itemsPerPage, $lastPage); +// Compte le nombre de produits dans chaque catégorie +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Exemples d'utilisation : +Définit une condition pour le filtrage des lignes groupées (HAVING). Elle peut être utilisée en combinaison avec la méthode `group()` et les fonctions d'agrégation : ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Recherche les catégories contenant plus de 100 produits +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -ayant() .[#toc-having] ----------------------- +Lecture des données +=================== + +Pour lire les données de la base de données, plusieurs méthodes utiles sont disponibles : + +.[language-php] +| `foreach ($table as $key => $row)` | Interroge toutes les lignes, `$key` est la valeur de la clé primaire, `$row` est un objet ActiveRow. +| `$row = $table->get($key)` | Retourne une seule ligne par clé primaire. +| `$row = $table->fetch()` | Retourne la ligne courante et avance le pointeur à la ligne suivante. +| `$array = $table->fetchPairs()` | Crée un tableau associatif à partir des résultats. +| `$array = $table->fetchAll()` | Retourne toutes les lignes sous forme de tableau + `count($table)` | Retourne le nombre de lignes dans l'objet Sélection | | Retourne le nombre de lignes dans l'objet Sélection + +L'objet [ActiveRow |api:Nette\Database\Table\ActiveRow] est en lecture seule. Cela signifie que vous ne pouvez pas modifier les valeurs de ses propriétés. Cette restriction garantit la cohérence des données et évite les effets secondaires inattendus. Les données sont extraites de la base de données et toute modification doit être effectuée de manière explicite et contrôlée. + + +`foreach` - Itération sur toutes les lignes +------------------------------------------- -Exemples d'utilisation : +La manière la plus simple d'exécuter une requête et de récupérer des lignes est d'itérer avec la boucle `foreach`. Elle exécute automatiquement la requête SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = clé primaire, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtrage par une autre valeur de la table .[#toc-joining-key] -------------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Exécute une requête SQL et renvoie une ligne par sa clé primaire ou `null` si elle n'existe pas. + +```php +$book = $explorer->table('book')->get(123); // renvoie l'ActiveRow avec l'ID 123 ou null +if ($book) { + echo $book->title; +} +``` + -Vous avez souvent besoin de filtrer les résultats en fonction d'une condition qui implique une autre table de la base de données. Ces types de conditions nécessitent une jointure de table. Cependant, vous n'avez plus besoin de les écrire. +fetch(): ?ActiveRow .[method] +----------------------------- -Disons que vous devez obtenir tous les livres dont l'auteur s'appelle "Jon". Tout ce que vous devez écrire est la clé de jointure de la relation et le nom de la colonne dans la table jointe. La clé de jointure est dérivée de la colonne qui fait référence à la table que vous voulez joindre. Dans notre exemple (voir le schéma de la base de données), il s'agit de la colonne `author_id`, et il suffit d'en utiliser la première partie - `author` (le suffixe `_id` peut être omis). `name` est une colonne de la table `author` que nous souhaitons utiliser. Il est tout aussi facile de créer une condition pour le traducteur de livres (qui est relié par la colonne `translator_id` ). +Renvoie une ligne et fait passer le pointeur interne à la ligne suivante. S'il n'y a plus de lignes, il renvoie `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); +} ``` -La logique de la clé de jonction est pilotée par la mise en œuvre des [Conventions |api:Nette\Database\Conventions]. Nous vous encourageons à utiliser [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], qui analyse vos clés étrangères et vous permet de travailler facilement avec ces relations. -La relation entre le livre et son auteur est 1:N. La relation inverse est également possible. Nous l'appelons **backjoin**. Prenons un autre exemple. Nous souhaitons récupérer tous les auteurs qui ont écrit plus de 3 livres. Pour que la jointure soit inversée, nous utilisons l'instruction `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY`. La condition doit également être écrite sous la forme d'une instruction `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Renvoie les résultats sous la forme d'un tableau associatif. Le premier argument spécifie le nom de la colonne à utiliser comme clé dans le tableau, et le second argument spécifie le nom de la colonne à utiliser comme valeur : ```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', ...] ``` -Vous avez peut-être remarqué que l'expression de jointure fait référence au livre, mais il n'est pas clair si nous faisons la jointure par `author_id` ou `translator_id`. Dans l'exemple ci-dessus, Selection fait la jointure par la colonne `author_id` parce qu'une correspondance avec la table source a été trouvée - la table `author`. Si une telle correspondance n'avait pas été trouvée et qu'il y avait d'autres possibilités, Nette aurait lancé [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Si seule la colonne clé est spécifiée, la valeur sera la ligne entière, c'est-à-dire l'objet `ActiveRow`: -Pour effectuer une jointure via la colonne `translator_id`, fournissez un paramètre facultatif dans l'expression de jointure. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +Si `null` est spécifié comme clé, le tableau sera indexé numériquement à partir de zéro : ```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', ...] ``` -Examinons maintenant des expressions de jointure plus complexes. +Vous pouvez également passer un callback en paramètre, qui renverra soit la valeur elle-même, soit une paire clé-valeur pour chaque ligne. Si le callback ne renvoie qu'une valeur, la clé sera la clé primaire de la ligne : + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Premier livre (Jan Novak)', ...] + +// Le callback peut également renvoyer un tableau avec une paire clé/valeur: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Premier livre' => 'Jan Novak', ...] +``` -Nous aimerions trouver tous les auteurs qui ont écrit quelque chose sur PHP. Tous les livres ont une étiquette, nous devons donc sélectionner les auteurs qui ont écrit un livre avec l'étiquette PHP. + +fetchAll(): array .[method] +--------------------------- + +Renvoie toutes les lignes sous la forme d'un tableau associatif d'objets `ActiveRow`, dont les clés sont les valeurs de la clé primaire. ```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, ...), ...] ``` -Requêtes agrégées .[#toc-aggregate-queries] -------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Obtenir le nombre de lignes -| `$table->count("DISTINCT $column")` | Obtenir le nombre de valeurs distinctes -| `$table->min($column)` | Obtenir la valeur minimale -| `$table->max($column)` | Obtenir la valeur maximale -| `$table->sum($column)` | Obtenir la somme de toutes les valeurs -| `$table->aggregation("GROUP_CONCAT($column)")` | Exécuter une fonction d'agrégation quelconque +La méthode `count()` sans paramètres renvoie le nombre de lignes de l'objet `Selection`: -.[caution] -La méthode `count()` sans aucun paramètre spécifié sélectionne tous les enregistrements et renvoie la taille du tableau, ce qui est très inefficace. Par exemple, si vous devez calculer le nombre de lignes pour la pagination, spécifiez toujours le premier argument. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternative +``` +Remarque : `count()` avec un paramètre exécute la fonction d'agrégation COUNT dans la base de données, comme décrit ci-dessous. -Échappement et citation .[#toc-escaping-quoting] -================================================ -Database Explorer est intelligent et échappe les paramètres et les identificateurs de citation pour vous. Ces règles de base doivent cependant être respectées : +ActiveRow::toArray(): array .[method] +------------------------------------- -- les mots-clés, fonctions, procédures doivent être en majuscules -- les colonnes et les tables doivent être en minuscules -- passer des variables comme paramètres, ne pas les concaténer +Convertit l'objet `ActiveRow` en un tableau associatif dont les clés sont les noms de colonnes et les valeurs les données correspondantes. ```php -->where('name like ?', 'John'); // FAUX ! génère: `name` `like` ? -->where('name LIKE ?', 'John'); // CORRECT +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray sera ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + -->where('KEY = ?', $value); // FAUX ! KEY est un mot-clé -->where('key = ?', $value); // CORRECT. génère: `key` = ? +Agrégation .[#toc-aggregation] +============================== -->where('name = ' . $name); // FAUX ! injection sql ! -->where('name = ?', $name); // CORRECT +La classe `Selection` fournit des méthodes permettant d'exécuter facilement des fonctions d'agrégation (COUNT, SUM, MIN, MAX, AVG, etc.). -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // FAUX ! passer des variables comme paramètres, ne pas concaténer -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORRECT +.[language-php] +| `count($expr)` | Compte le nombre de lignes. +| `min($expr)` | Renvoie la valeur minimale d'une colonne. +| `max($expr)` | Renvoie la valeur maximale d'une colonne. +| `sum($expr)` | Retourne la somme des valeurs d'une colonne +| `aggregation($function)` | Permet toute fonction d'agrégation, telle que `AVG()` ou `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Exécute une requête SQL avec la fonction COUNT et renvoie le résultat. Cette méthode est utilisée pour déterminer le nombre de lignes correspondant à une certaine condition : + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` (Comptage) +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Remarque : [count() |#count()] sans paramètre renvoie simplement le nombre de lignes dans l'objet `Selection`. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Les méthodes `min()` et `max()` renvoient les valeurs minimales et maximales de la colonne ou de l'expression spécifiée : + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Renvoie la somme des valeurs de la colonne ou de l'expression spécifiée : + +```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): mixed .[method] +----------------------------------------------------------------------------- + +Permet l'exécution de n'importe quelle fonction d'agrégation. + +```php +// Calcule le prix moyen des produits d'une catégorie +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Combine les étiquettes de produits en une seule chaîne de caractères +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Si nous devons agréger des résultats qui résultent eux-mêmes d'une agrégation et d'un regroupement (par exemple, `SUM(value)` sur des lignes regroupées), nous spécifions la fonction d'agrégation à appliquer à ces résultats intermédiaires en tant que deuxième argument : + +```php +// Calcule le prix total des produits en stock pour chaque catégorie, puis fait la somme de ces prix. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); ``` -.[warning] -Une mauvaise utilisation peut entraîner des failles de sécurité +Dans cet exemple, nous calculons d'abord le prix total des produits dans chaque catégorie (`SUM(price * stock) AS category_total`) et nous regroupons les résultats par `category_id`. Nous utilisons ensuite `aggregation('SUM(category_total)', 'SUM')` pour additionner ces sous-totaux. Le deuxième argument `'SUM'` spécifie la fonction d'agrégation à appliquer aux résultats intermédiaires. -Récupération des données .[#toc-fetching-data] -============================================== +Insérer, mettre à jour et supprimer .[#toc-insert-update-delete] +================================================================ + +Nette Database Explorer simplifie l'insertion, la mise à jour et la suppression de données. Toutes les méthodes mentionnées envoient un message `Nette\Database\DriverException` en cas d'erreur. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- -| `foreach ($table as $id => $row)` | Itérer sur toutes les lignes du résultat -| `$row = $table->get($id)` | Récupérer une seule ligne avec l'ID $id dans le tableau. -| `$row = $table->fetch()` | Récupérer la ligne suivante dans le résultat -| `$array = $table->fetchPairs($key, $value)` | Récupérer toutes les valeurs dans un tableau associatif -| `$array = $table->fetchPairs($value)` | Récupère toutes les lignes dans un tableau associatif -| `count($table)` | Obtenir le nombre de lignes dans le jeu de résultats +Insère de nouveaux enregistrements dans une table. +**Insertion d'un seul enregistrement:** -Insertion, mise à jour et suppression .[#toc-insert-update-delete] -================================================================== +Le nouvel enregistrement est transmis sous la forme d'un tableau associatif ou d'un objet itérable (tel que `ArrayHash` utilisé dans les [formulaires |forms:]), dont les clés correspondent aux noms des colonnes de la table. -La méthode `insert()` accepte un tableau d'objets Traversables (par exemple [ArrayHash |utils:arrays#ArrayHash] qui renvoie des [formes |forms:]) : +Si la table possède une clé primaire définie, la méthode renvoie un objet `ActiveRow`, qui est rechargé à partir de la base de données afin de refléter toute modification apportée au niveau de la base de données (par exemple, déclencheurs, valeurs de colonne par défaut ou calculs d'incrémentation automatique). Cela garantit la cohérence des données et l'objet contient toujours les données actuelles de la base de données. Si une clé primaire n'est pas explicitement définie, la méthode renvoie les données d'entrée sous la forme d'un tableau. ```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 est une instance d'ActiveRow contenant les données complètes de la ligne insérée, +// y compris l'identifiant généré automatiquement et toute modification apportée par les déclencheurs. +echo $row->id; // Affiche l'identifiant de l'utilisateur nouvellement inséré +echo $row->created_at; // Affiche l'heure de création si elle a été définie par un déclencheur. ``` -Si la clé primaire est définie sur la table, un objet ActiveRow contenant la ligne insérée est retourné. +**Insertion de plusieurs enregistrements à la fois:** -Insertion multiple : +La méthode `insert()` vous permet d'insérer plusieurs enregistrements à l'aide d'une seule requête SQL. Dans ce cas, elle renvoie le nombre de lignes insérées. ```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 sera égal à 2 +``` + +Vous pouvez également passer en paramètre un objet `Selection` contenant une sélection de données. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Des fichiers ou des objets DateTime peuvent être passés comme paramètres : +**Insertion de valeurs spéciales:** + +Les valeurs peuvent être des fichiers, des objets `DateTime` ou des lettres SQL : ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // ou $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // insère le fichier + 'name' => 'John', + 'created_at' => new DateTime, // convertit au format de la base de données + 'avatar' => fopen('image.jpg', 'rb'), // insère le contenu d'un fichier binaire + 'uuid' => $explorer::literal('UUID()'), // appelle la fonction UUID() ]); ``` -Mise à jour (renvoie le nombre de lignes affectées) : + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Met à jour les lignes d'une table en fonction d'un filtre spécifié. Renvoie le nombre de lignes effectivement modifiées. + +Les colonnes à mettre à jour sont transmises sous la forme d'un tableau associatif ou d'un objet itérable (tel que `ArrayHash` utilisé dans les [formulaires |forms:]), dont les clés correspondent aux noms des colonnes du tableau : ```php -$count = $explorer->table('users') - ->where('id', 10) // doit être appelé avant 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 ``` -Pour la mise à jour, nous pouvons utiliser les opérateurs `+=` a `-=`: +Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs `+=` et `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // voir += + 'points+=' => 1, // augmente la valeur de la colonne "points" de 1 + 'coins-=' => 1, // diminue la valeur de la colonne "pièces" de 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Suppression (renvoie le nombre de lignes supprimées) : + +Selection::delete(): int .[method] +---------------------------------- + +Supprime des lignes d'un tableau en fonction d'un filtre spécifié. Renvoie le nombre de lignes supprimées. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Lorsque vous appelez `update()` ou `delete()`, veillez à utiliser `where()` pour spécifier les lignes à mettre à jour ou à supprimer. Si `where()` n'est pas utilisé, l'opération sera effectuée sur l'ensemble du tableau ! + -Travailler avec des relations .[#toc-working-with-relationships] -================================================================ +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Met à jour les données d'une ligne de la base de données représentée par l'objet `ActiveRow`. Il accepte en paramètre des données itérables, dont les clés sont des noms de colonnes. Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs `+=` et `-=`: -A une relation .[#toc-has-one-relation] ---------------------------------------- -Une seule relation est un cas d'utilisation courant. Le livre *a un* auteur. Le livre *a un* traducteur. L'obtention d'une ligne liée se fait principalement par la méthode `ref()`. Elle accepte deux arguments : le nom de la table cible et la colonne de jonction source. Voir l'exemple : +Une fois la mise à jour effectuée, l'objet `ActiveRow` est automatiquement rechargé à partir de la base de données afin de refléter toutes les modifications apportées au niveau de la base de données (par exemple, les déclencheurs). La méthode renvoie `true` uniquement si une modification réelle des données a eu lieu. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // incrémente le nombre de vues +]); +echo $article->views; // Affiche le nombre de vues actuel ``` -Dans l'exemple ci-dessus, nous récupérons l'entrée relative à l'auteur dans la table `author`, la clé primaire de l'auteur est recherchée dans la colonne `book.author_id`. La méthode Ref() retourne une instance ActiveRow ou null s'il n'y a pas d'entrée appropriée. La ligne retournée est une instance d'ActiveRow, nous pouvons donc travailler avec elle de la même manière qu'avec l'entrée du livre. +Cette méthode ne met à jour qu'une ligne spécifique de la base de données. Pour les mises à jour en masse de plusieurs lignes, utilisez la méthode [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Supprime une ligne de la base de données représentée par l'objet `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Supprime le livre avec l'ID 1 +``` -// ou directement -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Cette méthode ne supprime qu'une ligne spécifique dans la base de données. Pour la suppression en bloc de plusieurs lignes, utilisez la méthode [Selection::delete() |#Selection::delete()]. + + +Relations entre les tables .[#toc-relationships-between-tables] +=============================================================== + +Dans les bases de données relationnelles, les données sont réparties entre plusieurs tables et reliées par des clés étrangères. Nette Database Explorer offre un moyen révolutionnaire de travailler avec ces relations - sans écrire de requêtes JOIN ni nécessiter de configuration ou de génération d'entités. + +Pour la démonstration, nous utiliserons la **base de données exemple**[(disponible sur GitHub |https://github.com/nette-examples/books]). La base de données comprend les tables suivantes : + +- `author` - auteurs et traducteurs (colonnes `id`, `name`, `web`, `born`) +- `book` - livres (colonnes `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - tags (colonnes `id`, `name`) +- `book_tag` - tableau de liens entre les livres et les tags (colonnes `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Structure de la base de données .<> + +Dans cet exemple de base de données de livres, nous trouvons plusieurs types de relations (simplifiées par rapport à la réalité) : + +- **Un-à-plusieurs (1:N)** - Chaque livre **a un** auteur ; un auteur peut écrire **plusieurs** livres. +- **Zéro-à-plusieurs (0:N)** - Un livre **peut avoir** un traducteur ; un traducteur peut traduire **plusieurs** livres. +- **Zéro-à-un (0:1)** - Un livre **peut avoir** une suite. +- **Many-to-many (M:N)** - Un livre **peut avoir plusieurs** étiquettes, et une étiquette peut être attribuée à **plusieurs** livres. + +Dans ces relations, il y a toujours une table **parent** et une table **enfant**. Par exemple, dans la relation entre les auteurs et les livres, la table `author` est le parent et la table `book` est l'enfant - vous pouvez considérer qu'un livre "appartient" toujours à un auteur. Cela se reflète également dans la structure de la base de données : la table enfant `book` contient la clé étrangère `author_id`, qui fait référence à la table parent `author`. + +Si nous voulons afficher les livres avec le nom de leurs auteurs, nous avons deux possibilités. Soit nous récupérons les données à l'aide d'une seule requête SQL avec un JOIN : + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Soit nous récupérons les données en deux étapes - d'abord les livres, puis leurs auteurs - et nous les assemblons en PHP : + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Le livre a aussi un traducteur, donc obtenir le nom du traducteur est assez facile. +La deuxième approche est, étonnamment, **plus efficace**. Les données ne sont récupérées qu'une seule fois et peuvent être mieux utilisées dans le cache. C'est exactement comme cela que fonctionne Nette Database Explorer - il s'occupe de tout sous le capot et vous fournit une API propre : + ```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 est un enregistrement de la table 'author'. + echo 'translated by: ' . $book->translator?->name; +} ``` -Tout cela est bien, mais c'est un peu lourd, ne pensez-vous pas ? Database Explorer contient déjà les définitions des clés étrangères, alors pourquoi ne pas les utiliser automatiquement ? Faisons cela ! -Si nous appelons une propriété qui n'existe pas, ActiveRow essaie de résoudre le nom de la propriété appelante comme une relation 'has one'. Obtenir cette propriété est la même chose qu'appeler la méthode ref() avec un seul argument. Nous appellerons le seul argument **key**. La clé sera résolue en relation particulière de clé étrangère. La clé passée est comparée aux colonnes de la ligne, et si elle correspond, la clé étrangère définie sur la colonne correspondante est utilisée pour obtenir les données de la table cible correspondante. Voir l'exemple : +Accès à la table des parents .[#toc-accessing-the-parent-table] +--------------------------------------------------------------- + +L'accès à la table parent est simple. Il s'agit de relations telles que *un livre a un auteur* ou *un livre peut avoir un traducteur*. L'enregistrement lié est accessible via la propriété d'objet `ActiveRow` - le nom de la propriété correspond au nom de la colonne de la clé étrangère sans le suffixe `id`: ```php -$book->author->name; -// identique à -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // recherche l'auteur via la colonne "author_id". +echo $book->translator?->name; // recherche le traducteur via la colonne "translator_id". ``` -L'instance ActiveRow n'a pas de colonne auteur. Toutes les colonnes de livres sont recherchées pour une correspondance avec *key*. Dans ce cas, la correspondance signifie que le nom de la colonne doit contenir la clé. Ainsi, dans l'exemple ci-dessus, la colonne `author_id` contient la chaîne 'auteur' et est donc recherchée par la clé 'auteur'. Si vous voulez obtenir le traducteur du livre, vous pouvez utiliser par exemple 'translator' comme clé, car la clé 'translator' correspondra à la colonne `translator_id`. Vous trouverez plus d'informations sur la logique de correspondance des clés dans le chapitre sur les [expressions de jointure |#joining-key]. +Lorsqu'il accède à la propriété `$book->author`, Explorer recherche une colonne de la table `book` qui contient la chaîne `author` (c'est-à-dire `author_id`). En fonction de la valeur de cette colonne, il extrait l'enregistrement correspondant de la table `author` et le renvoie sous la forme d'un objet `ActiveRow`. De même, `$book->translator` utilise la colonne `translator_id`. Comme la colonne `translator_id` peut contenir `null`, l'opérateur `?->` est utilisé. + +Une autre approche est fournie par la méthode `ref()`, qui accepte deux arguments - le nom de la table cible et la colonne de liaison - et renvoie une instance `ActiveRow` ou `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; // lien vers l'auteur +echo $book->ref('author', 'translator_id')->name; // lien vers le traducteur ``` -Si vous souhaitez récupérer plusieurs livres, vous devez utiliser la même approche. Nette Database Explorer recherchera les auteurs et les traducteurs pour tous les livres recherchés en une seule fois. +La méthode `ref()` est utile si l'accès basé sur les propriétés ne peut pas être utilisé, par exemple lorsque la table contient une colonne portant le même nom que la propriété (`author`). Dans les autres cas, il est recommandé d'utiliser l'accès par propriété pour une meilleure lisibilité. + +Explorer optimise automatiquement les requêtes de base de données. Lorsqu'il parcourt les livres et accède à leurs enregistrements connexes (auteurs, traducteurs), Explorer ne génère pas de requête pour chaque livre individuellement. Au lieu de cela, il n'exécute qu'une **requête SELECT pour chaque type de relation**, ce qui réduit considérablement la charge de la base de données. En voici un exemple : ```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; } ``` -Le code n'exécutera que ces 3 requêtes : +Ce code n'exécutera que trois requêtes optimisées de la base de données : + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +La logique d'identification de la colonne de liaison est définie par l'implémentation des [conventions |api:Nette\Database\Conventions]. Nous recommandons d'utiliser [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], qui analyse les clés étrangères et vous permet de travailler en toute transparence avec les relations de table existantes. -A plusieurs relations .[#toc-has-many-relation] ------------------------------------------------ -La relation "has many" est une relation inversée "has one". L'auteur *a* écrit *beaucoup* de livres. L'auteur *a* traduit *beaucoup* de livres. Comme vous pouvez le constater, ce type de relation est un peu plus difficile car la relation est "nommée" ("écrit", "traduit"). L'instance ActiveRow possède la méthode `related()`, qui renvoie un tableau d'entrées liées. Les entrées sont également des instances ActiveRow. Voir l'exemple ci-dessous : +Accès à la table enfant .[#toc-accessing-the-child-table] +--------------------------------------------------------- + +L'accès à la table des enfants se fait dans le sens inverse. Nous demandons maintenant *quels livres cet auteur a-t-il écrits* ou *quels livres ce traducteur a-t-il traduits*. Pour ce type de requête, nous utilisons la méthode `related()`, qui renvoie un objet `Selection` contenant les enregistrements correspondants. Voici un exemple : ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Sort tous les livres écrits par l'auteur foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Sort tous les livres traduits par l'auteur foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Méthode `related()` La méthode accepte la description complète de la jointure passée comme deux arguments ou comme un argument joint par un point. Le premier argument est la table cible, le second est la colonne cible. +La méthode `related()` accepte la description de la relation en tant qu'argument unique utilisant la notation par points ou en tant que deux arguments distincts : ```php -$author->related('book.translator_id'); -// identique à -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // un seul argument +$author->related('book', 'translator_id'); // deux arguments ``` -Vous pouvez utiliser l'heuristique de Nette Database Explorer basée sur les clés étrangères et fournir uniquement l'argument **key**. La clé sera comparée à toutes les clés étrangères pointant vers la table actuelle (`author` table). S'il y a une correspondance, Nette Database Explorer utilisera cette clé étrangère, sinon, il lancera [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] ou [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Vous pouvez trouver plus d'informations sur la logique de correspondance des clés dans le chapitre sur les [expressions de jointure |#joining-key]. +Explorer peut détecter automatiquement la colonne de liaison correcte en se basant sur le nom de la table parente. Dans ce cas, il établit un lien via la colonne `book.author_id` car le nom de la table source est `author`: -Bien sûr, vous pouvez appeler les méthodes connexes pour tous les auteurs récupérés, Nette Database Explorer récupérera à nouveau les livres appropriés en une seule fois. +```php +$author->related('book'); // utilise book.author_id +``` + +S'il existe plusieurs connexions possibles, Explorer lèvera une exception [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Bien entendu, nous pouvons également utiliser la méthode `related()` lorsque nous parcourons plusieurs enregistrements dans une boucle, et Explorer optimisera automatiquement les requêtes dans ce cas également : ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -L'exemple ci-dessus n'exécutera que deux requêtes : +Ce code ne génère que deux requêtes SQL efficaces : ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Création manuelle de l'explorateur .[#toc-creating-explorer-manually] -===================================================================== +Relation de plusieurs à plusieurs .[#toc-many-to-many-relationship] +------------------------------------------------------------------- + +Pour une relation de plusieurs à plusieurs (M:N), une **table de jonction** (dans notre cas, `book_tag`) est nécessaire. Cette table contient deux colonnes de clés étrangères (`book_id`, `tag_id`). Chaque colonne fait référence à la clé primaire de l'une des tables connectées. Pour extraire des données connexes, nous récupérons d'abord les enregistrements de la table de jonction à l'aide de `related('book_tag')`, puis nous passons aux données cibles : + +```php +$book = $explorer->table('book')->get(1); +// Affiche les noms des étiquettes attribuées au livre +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // recherche le nom de l'étiquette dans la table des liens +} + +$tag = $explorer->table('tag')->get(1); +// Direction opposée: affiche les titres des livres avec cette balise +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // recherche le titre du livre +} +``` -Une connexion à la base de données peut être créée en utilisant la configuration de l'application. Dans ce cas, un service `Nette\Database\Explorer` est créé et peut être transmis comme dépendance à l'aide du conteneur DI. +Explorer optimise à nouveau les requêtes SQL pour les rendre plus efficaces : -Cependant, si Nette Database Explorer est utilisé comme un outil autonome, une instance de l'objet `Nette\Database\Explorer` doit être créée manuellement. +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Interroger des tables connexes .[#toc-querying-through-related-tables] +---------------------------------------------------------------------- + +Dans les méthodes `where()`, `select()`, `order()`, et `group()`, vous pouvez utiliser des notations spéciales pour accéder aux colonnes d'autres tables. Explorer crée automatiquement les JOINs nécessaires. + +La notation **point** (`parent_table.column`) est utilisée pour les relations 1:N vues du point de vue de la table mère : ```php -// $storage implémente Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Recherche les livres dont le nom de l'auteur commence par "Jon". +$books->where('author.name LIKE ?', 'Jon%'); + +// Trie les livres par nom d'auteur en ordre décroissant +$books->order('author.name DESC'); + +// Affiche le titre du livre et le nom de l'auteur +$books->select('book.title, author.name'); +``` + +**La notation par points** est utilisée pour les relations 1:N du point de vue de la table parente : + +```php +$authors = $explorer->table('author'); + +// Recherche les auteurs qui ont écrit un livre dont le titre contient "PHP". +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Compte le nombre de livres pour chaque auteur +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Dans l'exemple ci-dessus avec la notation deux points (`:book.title`), la colonne de la clé étrangère n'est pas explicitement spécifiée. Explorer détecte automatiquement la colonne correcte en se basant sur le nom de la table parente. Dans ce cas, il effectue la jointure via la colonne `book.author_id` car le nom de la table source est `author`. S'il existe plusieurs connexions possibles, Explorer lève l'[exception AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +La colonne de liaison peut être explicitement spécifiée entre parenthèses : + +```php +// Recherche les auteurs qui ont traduit un livre dont le titre contient "PHP". +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Les notations peuvent être enchaînées pour accéder aux données de plusieurs tables : + +```php +// Trouve les auteurs des livres étiquetés avec 'PHP'. +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Extension des conditions pour JOIN .[#toc-extending-conditions-for-join] +------------------------------------------------------------------------ + +La méthode `joinWhere()` ajoute des conditions supplémentaires aux jointures de tables en SQL après le mot-clé `ON`. + +Par exemple, supposons que nous voulions trouver des livres traduits par un traducteur spécifique : + +```php +// Trouve les livres traduits par un traducteur nommé 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +Dans la condition `joinWhere()`, vous pouvez utiliser les mêmes constructions que dans la méthode `where()` - opérateurs, caractères génériques, tableaux de valeurs ou expressions SQL. + +Pour les requêtes plus complexes avec plusieurs JOIN, des alias de table peuvent être définis : + +```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) +``` + +Notez que la méthode `where()` ajoute des conditions à la clause `WHERE`, tandis que la méthode `joinWhere()` étend les conditions de la clause `ON` lors des jointures de tables. + + +Création manuelle de l'explorateur .[#toc-manually-creating-explorer] +===================================================================== + +Si vous n'utilisez pas le conteneur Nette DI, vous pouvez créer manuellement une instance de `Nette\Database\Explorer`: + +```php +use Nette\Database; + +// $storage implémente Nette\Caching\Storage, par exemple: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// connexion à la base de données +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// gère la réflexion sur la structure de la base de données +$structure = new Database\Structure($connection, $storage); +// définit les règles de mappage des noms de tables, des colonnes et des clés étrangères +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/fr/security.texy b/database/fr/security.texy new file mode 100644 index 0000000000..7a4c440614 --- /dev/null +++ b/database/fr/security.texy @@ -0,0 +1,145 @@ +Risques de sécurité +******************* + +.[perex] +Les bases de données contiennent souvent des données sensibles et permettent des opérations dangereuses. Nette Database offre un certain nombre de fonctions de sécurité. Toutefois, il est essentiel de comprendre la différence entre les API sûres et non sûres. + + +Injection SQL .[#toc-sql-injection] +=================================== + +L'injection SQL est le risque de sécurité le plus sérieux lorsque l'on travaille avec des bases de données. Elle se produit lorsque des données utilisateur non vérifiées sont intégrées à une requête SQL. Un pirate peut alors injecter ses propres commandes SQL, ce qui lui permet d'obtenir ou de modifier des données dans la base de données. + +```php +// ❌ UNSAFE CODE - vulnérable à l'injection SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// L'attaquant peut saisir quelque chose comme: ' OR '1'='1 +// La requête résultante sera: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Cela renvoie tous les utilisateurs ! +``` + +Il en va de même pour Database Explorer : + +```php +// ❌ UNSAFE CODE +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Requêtes paramétrées sûres .[#toc-safe-parameterized-queries] +============================================================= + +La manière la plus sûre d'insérer des valeurs dans les requêtes SQL est d'utiliser des requêtes paramétrées. Nette Database propose plusieurs façons de les utiliser. + + +Points d'interrogation .[#toc-placeholder-question-marks] +--------------------------------------------------------- + +La méthode la plus simple est d'utiliser des points d'interrogation à la place du texte : + +```php +// ✅ Requêtes paramétrées sûres +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Condition de sécurité dans l'explorateur +$table->where('name = ?', $_GET['name']); +``` + +Il en va de même pour toutes les autres méthodes de l'Explorateur de bases de données qui permettent d'insérer des expressions avec des points d'interrogation et des paramètres. + +.[warning] +Les valeurs doivent être de type scalaire (`string`, `int`, `float`, `bool`) ou `null`. Si, par exemple, `$_GET['name']` est un tableau, Nette Database inclura tous ses éléments dans la requête SQL, ce qui peut être indésirable. + + +Tableaux de valeurs .[#toc-value-arrays] +---------------------------------------- + +Pour les clauses `INSERT`, `UPDATE`, ou `WHERE`, nous pouvons utiliser des tableaux de valeurs : + +```php +// ✅ INSERT sûr +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ UPDATE sûr +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database échappe automatiquement toutes les valeurs passées dans les requêtes paramétrées. Cependant, nous devons nous assurer que le type de données des paramètres est correct. + + +Les clés de tableau ne sont pas une API sûre .[#toc-array-keys-are-not-a-safe-api] +================================================================================== + +Si les valeurs des tableaux sont sûres, il n'en va pas de même pour les clés : + +```php +// ❌ UNSAFE CODE - les clés peuvent contenir une injection SQL +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Pour les commandes `INSERT` et `UPDATE`, il s'agit d'une faille de sécurité critique : un pirate pourrait insérer ou modifier n'importe quelle colonne de la base de données. Par exemple, il pourrait définir `is_admin = 1` ou insérer des données arbitraires dans des colonnes sensibles. + +Dans les conditions de `WHERE`, c'est encore plus dangereux car cela permet une **énumération SQL** - une technique pour récupérer progressivement des informations sur la base de données. Un attaquant pourrait tenter d'explorer les salaires des employés en injectant dans `$_GET` ce qui suit : + +```php +$_GET = ['salary >', 100000]; // commence à déterminer les fourchettes de salaires +``` + +Le principal problème, cependant, est que les conditions `WHERE` supportent les expressions SQL dans les clés : + +```php +// Utilisation légitime des opérateurs dans les clés +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ INCONNU: l'attaquant peut injecter son propre code SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // permet à l'attaquant d'obtenir les salaires des administrateurs +``` + +Il s'agit encore une fois d'une **injection SQL**. + + +Liste blanche de colonnes .[#toc-whitelisting-columns] +------------------------------------------------------ + +Si vous souhaitez permettre aux utilisateurs de choisir des colonnes, utilisez toujours une liste blanche : + +```php +// ✅ Traitement sécurisé - seules les colonnes autorisées +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Identificateurs dynamiques .[#toc-dynamic-identifiers] +====================================================== + +Pour les noms de tables et de colonnes dynamiques, utilisez l'espace réservé `?name`: + +```php +// ✅ Utilisation sûre d'identifiants de confiance +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - ne jamais utiliser les données de l'utilisateur +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Le symbole `?name` ne doit être utilisé que pour les valeurs de confiance définies dans le code de l'application. Pour les valeurs fournies par l'utilisateur, utilisez à nouveau une liste blanche. diff --git a/database/hu/@left-menu.texy b/database/hu/@left-menu.texy index a79f4b4cce..9672a5a522 100644 --- a/database/hu/@left-menu.texy +++ b/database/hu/@left-menu.texy @@ -4,3 +4,4 @@ Adatbázis - [Felfedező |Explorer] - [Reflexió |Reflection] - [Konfiguráció |Configuration] +- [Biztonsági kockázatok |security] diff --git a/database/hu/explorer.texy b/database/hu/explorer.texy index cab2efd2d7..a1fee6f1e6 100644 --- a/database/hu/explorer.texy +++ b/database/hu/explorer.texy @@ -1,550 +1,929 @@ -Adatbázis-kutató -**************** +Database Explorer +*****************
-A Nette Database Explorer jelentősen leegyszerűsíti az adatok lekérdezését az adatbázisból SQL-lekérdezések írása nélkül. +A Nette Database Explorer egy hatékony réteg, amely jelentősen leegyszerűsíti az adatlekérdezést az adatbázisból anélkül, hogy SQL-lekérdezéseket kellene írni. -- hatékony lekérdezéseket használ -- nem továbbít feleslegesen adatokat -- elegáns szintaxissal rendelkezik +- Az adatokkal való munka természetes és könnyen érthető +- Optimalizált SQL-lekérdezéseket generál, amelyek csak a szükséges adatokat hívják le +- Könnyű hozzáférést biztosít a kapcsolódó adatokhoz JOIN lekérdezések írása nélkül +- Azonnal működik konfiguráció vagy entitásgenerálás nélkül
-Az Adatbázis-kutató használatához kezdje egy táblával - hívja meg a `table()` címet a [api:Nette\Database\Explorer] objektumon. A kontextusobjektum-példány megszerzésének legegyszerűbb módja [itt van leírva |core#Connection and Configuration], vagy abban az esetben, ha a Nette Database Explorer önálló eszközként használatos, [kézzel is létrehozható |#Creating Explorer Manually]. +A Nette Database Explorer az alacsony szintű [Nette Database Core |core] réteg kiterjesztése, amely kényelmes objektumorientált megközelítést ad az adatbázis-kezeléshez. + +Az Explorerrel való munka a `table()` metódus meghívásával kezdődik a [api:Nette\Database\Explorer] objektumon (annak megszerzése [itt van leírva |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // db tábla neve 'book' +$books = $explorer->table('book'); // 'book' a táblázat neve ``` -A hívás a [Selection |api:Nette\Database\Table\Selection] objektum egy példányát adja vissza, amelyen iterálva lekérdezhetjük az összes könyvet. Minden egyes elemet (sort) egy [ActiveRow |api:Nette\Database\Table\ActiveRow] példány képvisel, amelynek tulajdonságaihoz adatokat rendelünk: +A módszer egy [Selection |api:Nette\Database\Table\Selection] objektumot ad vissza, amely egy SQL-lekérdezést reprezentál. Az eredmények szűréséhez és rendezéséhez további metódusok kapcsolhatók ehhez az objektumhoz. A lekérdezés csak akkor áll össze és hajtódik végre, amikor az adatokat kérik, például a `foreach` iterálásával. Minden sort egy [ActiveRow |api:Nette\Database\Table\ActiveRow] objektum képvisel: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // a "title" oszlop kimenete + echo $book->author_id; // kimenet 'author_id' oszlop } ``` -Csak egy adott sor kinyerése a `get()` metódussal történik, amely közvetlenül egy ActiveRow példányt ad vissza. +Az Explorer jelentősen leegyszerűsíti a [táblázatos kapcsolatokkal |#Vazby mezi tabulkami] való munkát. A következő példa azt mutatja, hogy milyen egyszerűen adhatunk ki adatokat a kapcsolódó táblákból (könyvek és szerzőik). Vegyük észre, hogy nem kell JOIN-lekérdezéseket írni; a Nette generálja ezeket helyettünk: ```php -$book = $explorer->table('book')->get(2); // visszaadja a 2 azonosítóval rendelkező könyvet. -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // JOIN-t hoz létre a 'author' táblához +} ``` -Vessünk egy pillantást a gyakori felhasználási esetre. Könyveket és szerzőiket kell lekérni. Ez egy gyakori 1:N kapcsolat. A gyakran használt megoldás az adatok lekérdezése egyetlen SQL-lekérdezéssel, táblázat-összekötésekkel. A másik lehetőség, hogy az adatokat külön-külön lekérdezzük, egy lekérdezést futtatunk a könyvek lekérdezésére, majd egy másik lekérdezéssel (pl. a foreach ciklusban) minden könyvhöz megkapjuk a szerzőt. Ez könnyen optimalizálható úgy, hogy csak két lekérdezés fusson, egy a könyvekre és egy másik a szükséges szerzőkre - és a Nette Database Explorer pontosan így csinálja. +A Nette Database Explorer optimalizálja a lekérdezéseket a maximális hatékonyság érdekében. A fenti példa csak két SELECT-lekérdezést hajt végre, függetlenül attól, hogy 10 vagy 10 000 könyvet dolgozunk fel. -Az alábbi példákban az ábrán látható adatbázis-sémával fogunk dolgozni. Vannak OneHasMany (1:N) linkek (a könyv szerzője `author_id` és az esetleges fordító `translator_id`, amely lehet `null`) és ManyHasMany (M:N) link a könyv és a címkék között. +Ezenkívül az Explorer nyomon követi, hogy a kódban mely oszlopokat használjuk, és csak azokat hívja le az adatbázisból, ami további teljesítményt takarít meg. Ez a viselkedés teljesen automatikus és adaptív. Ha később módosítjuk a kódot, hogy további oszlopokat használjunk, az Explorer automatikusan módosítja a lekérdezéseket. Nem kell semmit sem konfigurálnia, vagy gondolkodnia azon, hogy mely oszlopokra lesz szükség - ezt bízza a Nette-re. -[Egy sémát is tartalmazó példa megtalálható a GitHubon |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** A példákban használt adatbázis-struktúra .<> +Szűrés és rendezés .[#toc-filtering-and-sorting] +================================================ -A következő kód minden könyvhöz felsorolja a szerző nevét és az összes címkét. [Mindjárt megbeszéljük |#Working with relationships], hogyan működik ez belsőleg. +A `Selection` osztály metódusokat biztosít az adatok szűrésére és rendezésére. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | WHERE feltétel hozzáadása. Több feltétel az AND segítségével kombinálható | +| `whereOr(array $conditions)` | WHERE feltételek csoportjának hozzáadása, melyeket OR segítségével kombinálnak | +| `wherePrimary($value)` | WHERE feltétel hozzáadása az elsődleges kulcs alapján | +| `order($columns, ...$params)` | Rendezés beállítása ORDER BY segítségével | +| `select($columns, ...$params)` | Meghatározza, hogy mely oszlopokat kérje le | +| `limit($limit, $offset = null)` | Korlátozza a sorok számát (LIMIT) és opcionálisan beállítja az OFFSET-et | +| `page($page, $itemsPerPage, &$total = null)` | Beállítja a lapozást | +| `group($columns, ...$params)` | Sorok csoportosítása (GROUP BY) | +| `having($condition, ...$params)`| Hozzáad egy HAVING feltételt a csoportosított sorok szűréséhez | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'írta: ' . $book->author->name; // $book->szerző a 'szerző' táblázat sora. +A metódusok láncolhatók (ún. [folyékony interfész |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 egy sor a 'tag' táblázatból. - } -} -``` +Ezek a módszerek speciális jelölések használatát is lehetővé teszik a [kapcsolódó táblák adatainak |#Dotazování přes související tabulky] eléréséhez. -Örülni fog, hogy milyen hatékonyan működik az adatbázis-réteg. A fenti példa állandó számú kérést tesz, amelyek így néznek ki: -```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)) -``` +Kikerülés és azonosítók .[#toc-escaping-and-identifiers] +-------------------------------------------------------- -Ha a [gyorsítótárat |caching:] használja (alapértelmezés szerint be van kapcsolva), egyetlen oszlopot sem kérdez le feleslegesen. Az első lekérdezés után a gyorsítótár tárolja a használt oszlopneveket, és a Nette Database Explorer csak a szükséges oszlopokkal kapcsolatos lekérdezéseket hajtja végre: +A módszerek automatikusan kikerülik a paramétereket és az idézőjeles azonosítókat (táblázat- és oszlopnevek), megakadályozva ezzel az SQL injektálást. A megfelelő működés biztosítása érdekében néhány szabályt be kell tartani: -```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)) +- A kulcsszavakat, függvényneveket, eljárásokat stb. **uppercase**-ben írjuk. +- Az oszlop- és táblázatok neveit **kisbetűvel** írja. +- Mindig **paraméterek** használatával adjon át karakterláncokat. + +```php +where('name = ' . $name); // **DISASTER**: sebezhető SQL injekcióval szemben +where('name LIKE "%search%"'); // **WRONG**: megnehezíti az automatikus idézést +where('name LIKE ?', '%search%'); // **CORRECT**: paraméterként átadott érték + +where('name like ?', $name); // **HIBA**: generál: `név` `mint` ? +where('name LIKE ?', $name); // **CORRECT**: generál: `név` LIKE ? +where('LOWER(name) = ?', $value);// **TÖRVÉNYES**: LOWER(`név`) = ? ``` -Kiválasztások .[#toc-selections] -================================ +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Lásd a sorok szűrésének és korlátozásának lehetőségeit [api:Nette\Database\Table\Selection]: +Az eredmények szűrése WHERE feltételekkel. Erőssége a különböző értéktípusok intelligens kezelésében és az SQL-operátorok automatikus kiválasztásában rejlik. -.[language-php] -| `$table->where($where[, $param[, ...]])` | WHERE beállítása AND ragasztóval, ha két vagy több feltétel van megadva -| `$table->whereOr($where)` | WHERE beállítása, amely két vagy több feltétel megadása esetén OR-t használ ragasztóként. -| `$table->order($columns)` | ORDER BY beállítása, lehet kifejezés. `('column DESC, id DESC')` -| `$table->select($columns)` | Letöltött oszlopok beállítása, lehet kifejezés is. `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | LIMIT és OFFSET beállítása -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Engedélyezi a lapozást -| `$table->group($columns)` | GROUP BY beállítása -| `$table->having($having)` | HAVING beállítása +Alapvető használat: -Használhatunk egy úgynevezett [folyékony felületet |nette:introduction-to-object-oriented-programming#fluent-interfaces], például a `$table->where(...)->order(...)->limit(...)`. Több `where` vagy `whereOr` feltételt a `AND` operátorral kapcsolunk össze. +```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' +``` +A megfelelő operátorok automatikus felismerésének köszönhetően nem kell speciális eseteket kezelnie - a Nette kezeli ezeket Ön helyett: -where() .[#toc-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) +// A helyőrző ? operátor nélkül is használható: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -A Nette Database Explorer automatikusan hozzá tudja adni a szükséges operátorokat az átadott értékekhez: +A módszer a negatív feltételeket és az üres tömböket is helyesen kezeli: -.[language-php] -| `$table->where('field', $value)` | field = $value -| `$table->where('field', null)` | field IS NULL -| `$table->where('field > ?', $val)` | mező > $val -| `$table->where('field', [1, 2])` | field IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 VAGY név = '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 -- nem talál semmit +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- mindent megtalál +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- mindent megtalál. +// $table->where('NOT id ?', $ids); // FIGYELEM: Ez a szintaxis nem támogatott. +``` -A helyőrzőt oszlopoperátor nélkül is megadhatja. Ezek a hívások ugyanazok. +Egy másik táblázat lekérdezésének eredményét is átadhatja paraméterként, al-lekérdezést létrehozva: ```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')); ``` -Ez a funkció lehetővé teszi a helyes operátor generálását az érték alapján: +A feltételek tömbként is átadhatók, az elemeket pedig AND segítségével kombinálhatjuk: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`ár_végleges` < `ár_eredeti`) AND (`készlet_szám` > `min_készlet`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -A kiválasztás helyesen kezeli a negatív feltételeket is, üres tömbök esetén is működik: +A tömbben kulcs-érték párokat lehet használni, és a Nette ismét automatikusan kiválasztja a megfelelő operátorokat: ```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], +]); +``` -// ez kivételt dob, ez a szintaxis nem támogatott. -$table->where('NOT id ?', $ids); +SQL-kifejezéseket is keverhetünk helyőrzővel és több paraméterrel. Ez a pontosan meghatározott operátorokkal rendelkező összetett feltételeknél hasznos: + +```php +// WHERE (`életkor` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // két paramétert adunk át tömbként +]); ``` +A `where()` többszöri hívása automatikusan kombinálja a feltételeket az AND segítségével. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Példa a paraméterek nélküli használatra: +Hasonló a `where()`-hoz, de a feltételeket OR segítségével kombinálja: ```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, ]); ``` -A paramétereket használjuk. Ha nem ad meg operátort, a Nette Database Explorer automatikusan hozzáadja a megfelelőt: +Bonyolultabb kifejezések is használhatók: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`ár` > 1000) OR (`ár_adóval együtt` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -A kulcs tartalmazhat egy joker kérdőjeleket tartalmazó kifejezést, majd az értékben paramétereket adhat át: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Feltételt ad a táblázat elsődleges kulcsához: ```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]); +``` + +Ha a táblának összetett elsődleges kulcsa van (pl. `foo_id`, `bar_id`), akkor azt tömbként adjuk át: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Felhasználási példák: +Megadja a sorok visszaadási sorrendjét. Rendezhet egy vagy több oszlop szerint, növekvő vagy csökkenő sorrendben, vagy egyéni kifejezéssel: ```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 "kreált +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDER BY `prioritás` DESC, `létrehozva` +$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC ``` -select() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Megadja az adatbázisból visszaadandó oszlopokat. Alapértelmezés szerint a Nette Database Explorer csak a kódban ténylegesen használt oszlopokat adja vissza. Használja a `select()` módszert, ha konkrét kifejezések lekérdezésére van szüksége: + +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` (formázott dátum) +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` -Felhasználási példák: +A `AS` segítségével definiált aliasok ezután a `ActiveRow` objektum tulajdonságaiként érhetők el: ```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; // hozzáférés az aliashoz +} ``` -limit() .[#toc-limit] ---------------------- +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Felhasználási példák: +Korlátozza a visszaadott sorok számát (LIMIT) és opcionálisan beállít egy eltolást: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (az első 10 sort adja vissza) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Oldalszámozáshoz célszerűbb a `page()` módszert használni. -page() .[#toc-page] -------------------- -A határérték és az eltolás beállításának alternatív módja: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Egyszerűsíti az eredmények oldalszámozását. Elfogadja az oldalszámot (1-től kezdődően) és az oldalankénti elemek számát. Opcionálisan átadhat egy hivatkozást egy változóra, ahol az oldalak teljes száma tárolódik: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -A `$lastPage` változónak átadott utolsó oldalszám megadása: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Csoportosítja a sorokat a megadott oszlopok szerint (GROUP BY). Általában aggregáló függvényekkel együtt használják: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Megszámolja az egyes kategóriákba tartozó termékek számát +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Felhasználási példák: +Feltételt állít be a csoportosított sorok szűrésére (HAVING). A `group()` módszerrel és az aggregáló függvényekkel együtt használható: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// 100-nál több terméket tartalmazó kategóriák keresése +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Adatok leolvasása +================= + +Az adatok adatbázisból való kiolvasásához több hasznos módszer áll rendelkezésre: + +.[language-php] +| `foreach ($table as $key => $row)` | Végigmegy az összes soron, `$key` az elsődleges kulcs értéke, `$row` egy ActiveRow objektum | +| `$row = $table->get($key)` | Visszaad egy sort az elsődleges kulcs alapján | +| `$row = $table->fetch()` | Visszaadja az aktuális sort, és a mutatót a következő sorra továbbítja | +| `$array = $table->fetchPairs()` | Az eredményekből asszociatív tömböt hoz létre | +| `$array = $table->fetchAll()` | Visszaadja az összes sort tömbként | +| `count($table)` | Visszaadja a Selection objektum sorainak számát | + +Az [ActiveRow |api:Nette\Database\Table\ActiveRow] objektum csak olvasható. Ez azt jelenti, hogy a tulajdonságainak értékeit nem lehet megváltoztatni. Ez a korlátozás biztosítja az adatok konzisztenciáját és megakadályozza a váratlan mellékhatásokat. Az adatok az adatbázisból kerülnek lekérdezésre, és minden változtatást kifejezetten és ellenőrzött módon kell végrehajtani. + + +`foreach` - Az összes soron való ismétlés +----------------------------------------- -Használati példák: +A lekérdezés végrehajtásának és a sorok kinyerésének legegyszerűbb módja az iterálás a `foreach` ciklus segítségével. Ez automatikusan végrehajtja az SQL-lekérdezést. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = elsődleges kulcs, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Szűrés egy másik táblázat értéke alapján .[#toc-joining-key] ------------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Végrehajt egy SQL-lekérdezést, és visszaad egy sort az elsődleges kulcs alapján, vagy ha nem létezik, akkor a `null` címen. + +```php +$book = $explorer->table('book')->get(123); // visszatér ActiveRow azonosítóval 123 vagy null +if ($book) { + echo $book->title; +} +``` -Gyakran előfordul, hogy az eredményeket olyan feltétel alapján kell szűrni, amely egy másik adatbázis-táblát érint. Az ilyen típusú feltételekhez táblaösszekötésre van szükség. Ezeket azonban már nem kell megírni. -Tegyük fel, hogy az összes olyan könyvet meg kell szereznünk, amelynek szerzőjének neve 'Jon'. Mindössze a kapcsolat összekötő kulcsát és az összekapcsolt tábla oszlopnevét kell megírnia. Az összekötő kulcs abból az oszlopból származik, amely az összekötni kívánt táblára utal. Példánkban (lásd a db sémát) ez a `author_id` oszlop, és elegendő, ha csak az első részét használjuk - `author` (a `_id` utótag elhagyható). A `name` a `author` tábla egyik oszlopa, amelyet használni szeretnénk. A könyvfordítóra vonatkozó feltétel (amelyhez a `translator_id` oszlop kapcsolódik) ugyanilyen egyszerűen létrehozható. +fetch(): ?ActiveRow .[method] +----------------------------- + +Visszaad egy sort, és a belső mutatót a következő sorra továbbítja. Ha nincs több sor, akkor visszaadja a `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); +} ``` -Az összekötő kulcs logikáját a [Conventions |api:Nette\Database\Conventions] megvalósítása vezérli. Javasoljuk a [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions] használatát, amely elemzi az idegen kulcsokat, és lehetővé teszi, hogy könnyen dolgozzon ezekkel a kapcsolatokkal. -A könyv és a szerzője közötti kapcsolat 1:N. A fordított kapcsolat is lehetséges. Ezt **backjoin**-nak nevezzük. Nézzünk meg egy másik példát. Szeretnénk lekérdezni az összes olyan szerzőt, aki több mint 3 könyvet írt. Ahhoz, hogy a join fordított legyen, a `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY` utasítást használjuk, a feltételt is `HAVING` utasítás formájában kell megírni. +fetchPairs(): array .[method] +----------------------------- + +Az eredményeket asszociatív tömbként adja vissza. Az első argumentum a tömb kulcsaként használandó oszlopnevet, a második argumentum pedig az értékként használandó oszlopnevet adja meg: ```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', ...] +``` + +Ha csak a kulcsoszlopot adjuk meg, az érték a teljes sor, azaz a `ActiveRow` objektum lesz: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Észrevehette, hogy az összekötő kifejezés a könyvre utal, de nem egyértelmű, hogy a `author_id` vagy a `translator_id` oldalon keresztül kötünk-e. A fenti példában a Selection a `author_id` oszlopon keresztül köt, mert találtunk egyezést a forrás táblával - a `author` táblával. Ha nem lenne ilyen egyezés, és több lehetőség is lenne, a Nette [AmbiguousReferenceKeyExceptiont |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] dobna. +Ha kulcsként a `null` van megadva, a tömb numerikusan indexelt lesz nullától kezdve: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` -A `translator_id` oszlopon keresztül történő csatlakozáshoz adjunk meg egy opcionális paramétert a csatlakozási kifejezésben. +Átadhat egy visszahívást is paraméterként, amely vagy magát az értéket, vagy egy kulcs-érték párt ad vissza minden egyes sorhoz. Ha a callback csak egy értéket ad vissza, a kulcs a sor elsődleges kulcsa lesz: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Első könyv (Jan Novak)', ...] + +// A visszahívás egy kulcs-érték párost tartalmazó tömböt is visszaadhat: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Első könyv' => 'Jan Novak', ...] ``` -Nézzünk meg néhány nehezebb csatlakozási kifejezést. -Szeretnénk megtalálni az összes szerzőt, aki írt valamit a PHP-ről. Minden könyvnek van címkéje, így ki kell választanunk azokat a szerzőket, akik PHP címkéjű könyvet írtak. +fetchAll(): array .[method] +--------------------------- + +Visszaadja az összes sort a `ActiveRow` objektumok asszociatív tömbjeként, ahol a kulcsok az elsődleges kulcsok értékei. ```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, ...), ...] ``` -Összesített lekérdezések .[#toc-aggregate-queries] --------------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Sorok számának lekérdezése -| `$table->count("DISTINCT $column")` | Különálló értékek számának lekérdezése -| `$table->min($column)` | Minimális érték lekérdezése -| `$table->max($column)` | Maximális érték lekérdezése -| `$table->sum($column)` | Az összes érték összegének lekérdezése -| `$table->aggregation("GROUP_CONCAT($column)")` | Bármilyen aggregációs függvény futtatása +A `count()` metódus paraméterek nélkül a `Selection` objektum sorainak számát adja vissza: -.[caution] -A `count()` módszer megadott paraméterek nélkül kiválasztja az összes rekordot és visszaadja a tömb méretét, ami nagyon nem hatékony. Ha például a lapozáshoz szükséges sorok számát kell kiszámítania, mindig adja meg az első argumentumot. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternatív +``` + +Megjegyzés: A `count()` paraméterrel a COUNT aggregációs funkciót hajtja végre az adatbázisban, az alábbiakban leírtak szerint. + + +ActiveRow::toArray(): array .[method] +------------------------------------- + +A `ActiveRow` objektumot egy asszociatív tömbre alakítja át, ahol a kulcsok az oszlopnevek, az értékek pedig a megfelelő adatok. +```php +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray lesz ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Összesítés .[#toc-aggregation] +============================== + +A `Selection` osztály metódusokat biztosít az aggregációs funkciók (COUNT, SUM, MIN, MAX, AVG stb.) egyszerű végrehajtásához. + +.[language-php] +| `count($expr)` | Számolja a sorok számát | +| `min($expr)` | Visszaadja a minimális értéket egy oszlopban | +| `max($expr)` | Visszaadja a maximális értéket egy oszlopban | +| `sum($expr)` | Visszaadja az értékek összegét egy oszlopban | +| `aggregation($function)` | Lehetővé tesz bármilyen aggregációs függvényt, például `AVG()` vagy `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +SQL-lekérdezést hajt végre a COUNT függvénnyel, és visszaadja az eredményt. Ez a módszer annak meghatározására szolgál, hogy hány sor felel meg egy adott feltételnek: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `oszlop`) FROM `tábla` +``` + +Megjegyzés: a [count() |#count()] paraméter nélkül egyszerűen a `Selection` objektumban lévő sorok számát adja vissza. -Kikerülés és idézés .[#toc-escaping-quoting] -============================================ -Az Adatbázis-kutató okos, és kikerüli a paramétereket és idézőjeleket azonosítókat az Ön helyett. Ezeket az alapvető szabályokat azonban be kell tartani: +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- -- kulcsszavak, függvények, eljárások nagybetűsek legyenek. -- az oszlopoknak és táblázatoknak kisbetűsnek kell lenniük -- a változókat paraméterként kell átadni, nem szabad összekapcsolni. +A `min()` és `max()` metódusok a megadott oszlop vagy kifejezés minimális és maximális értékét adják vissza: ```php -->where('name like ?', 'John'); // ROSSZ! generál: `név` `mint` ? -->where('name LIKE ?', 'John'); // HELYES +// SELECT MAX(`ár`) FROM `termék` WHERE `aktív` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + -->where('KEY = ?', $value); // ROSSZ! KEY egy kulcsszó -->where('key = ?', $value); // HELYES. generál: `key` = ? +sum(string $expr): int .[method] +-------------------------------- -->where('name = ' . $name); // ROSSZ! sql injection! -->where('name = ?', $name); // TÖRVÉNYES +A megadott oszlop vagy kifejezés értékeinek összegét adja vissza: -->select('DATE_FORMAT(created, "%d.%m.%m.%Y")'); // ROSSZ! változókat paraméterként átadni, ne kapcsoljuk össze! -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // HELYES +```php +// SELECT SUM(`ár` * `tételek_készleten`) FROM `termékek` WHERE `aktív` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -A helytelen használat biztonsági résekhez vezethet +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Lehetővé teszi bármilyen aggregációs függvény végrehajtását. -Adatok lekérése .[#toc-fetching-data] -===================================== +```php +// Kiszámítja a termékek átlagárát egy kategóriában +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -| `foreach ($table as $id => $row)` | Az eredmény összes során való ismételt átfutás -| `$row = $table->get($id)` | Egyetlen sor kinyerése $id azonosítóval a táblázatból -| `$row = $table->fetch()` | Következő sor kinyerése az eredményből. -| `$array = $table->fetchPairs($key, $value)` | Az összes érték beemelése asszociatív tömbbe. -| `$array = $table->fetchPairs($value)` | Minden sor lekérése asszociatív tömbbe. -| `count($table)` | Az eredményhalmaz sorainak számának kinyerése +// A termékcímkéket egyetlen karakterlánccá kombinálja +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Ha olyan eredményeket kell aggregálnunk, amelyek maguk is egy aggregálás és csoportosítás eredménye (pl. `SUM(value)` a csoportosított sorok felett), akkor második argumentumként megadjuk a közbenső eredményekre alkalmazandó aggregációs függvényt: + +```php +// Kiszámítja a készleten lévő termékek teljes árát kategóriánként, majd összegzi ezeket az árakat. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +Ebben a példában először kiszámítjuk az egyes kategóriákba tartozó termékek összárát (`SUM(price * stock) AS category_total`), majd az eredményeket a `category_id` alapján csoportosítjuk. Ezután a `aggregation('SUM(category_total)', 'SUM')` segítségével összegezzük ezeket a részösszegeket. A `'SUM'` második argumentum a közbenső eredményekre alkalmazandó aggregációs függvényt adja meg. Beszúrás, frissítés és törlés .[#toc-insert-update-delete] ========================================================== -A `insert()` módszer Traversable objektumok tömbjét fogadja el (például [ArrayHash |utils:arrays#ArrayHash], amely [űrlapokat |forms:] ad vissza): +A Nette Database Explorer leegyszerűsíti az adatok beszúrását, frissítését és törlését. Az összes említett módszer hiba esetén a `Nette\Database\DriverException` címet dobja. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Új rekordok beillesztése egy táblázatba. + +**Egyetlen rekord beillesztése:** + +Az új rekordot asszociatív tömbként vagy iterábilis objektumként (mint például az [űrlapokban |forms:] használt `ArrayHash` ) adja át, ahol a kulcsok megfelelnek a táblázat oszlopneveinek. + +Ha a táblának van definiált elsődleges kulcsa, a módszer egy `ActiveRow` objektumot ad vissza, amelyet az adatbázisból újratöltenek, hogy tükrözze az adatbázis szintjén végrehajtott változásokat (pl. triggerek, alapértelmezett oszlopértékek vagy automatikus növelési számítások). Ez biztosítja az adatok konzisztenciáját, és az objektum mindig az aktuális adatbázis-adatokat tartalmazza. Ha nincs explicit módon definiálva elsődleges kulcs, a metódus a bemeneti adatokat tömbként adja vissza. ```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 az ActiveRow egy példánya, amely a beillesztett sor teljes adatát tartalmazza, +// beleértve az automatikusan generált azonosítót és a triggerek által végrehajtott módosításokat is. +echo $row->id; // Kimeneti az újonnan beillesztett felhasználó azonosítóját +echo $row->created_at; // Kimeneti a létrehozás idejét, ha azt egy trigger állította be. ``` -Ha a táblázatban elsődleges kulcs van definiálva, akkor a rendszer egy ActiveRow objektumot ad vissza, amely a beillesztett sort tartalmazza. +**Egyszerre több rekord beillesztése:** -Többszörös beszúrás: +A `insert()` módszer lehetővé teszi több rekord beszúrását egyetlen SQL-lekérdezéssel. Ebben az esetben a beillesztett sorok számát adja vissza. ```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 `felhasználók` (`név`, `év`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows 2 lesz ``` -DateTime objektumok paraméterként átadhatók: +Paraméterként átadhat egy `Selection` objektumot is egy adatválasztékkal. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Speciális értékek beillesztése:** + +Az értékek lehetnek fájlok, `DateTime` objektumok vagy SQL literálok: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // vagy $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // beszúrja a fájlt. + 'name' => 'John', + 'created_at' => new DateTime, // átalakítja az adatbázis-formátumra + 'avatar' => fopen('image.jpg', 'rb'), // beilleszti a bináris fájl tartalmát + 'uuid' => $explorer::literal('UUID()'), // meghívja az UUID() függvényt ]); ``` -Frissítés (az érintett sorok számát adja vissza): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Egy megadott szűrő alapján frissíti egy táblázat sorait. A ténylegesen módosított sorok számát adja vissza. + +A frissítendő oszlopokat asszociatív tömbként vagy iterábilis objektumként (mint például az [űrlapokban |forms:] használt `ArrayHash` ) kell átadni, ahol a kulcsok megegyeznek a táblázat oszlopneveivel: ```php -$count = $explorer->table('users') - ->where('id', 10) // update() előtt kell meghívni. +$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 ``` -A frissítéshez használhatjuk a `+=` a `-=` operátorokat: +A numerikus értékek módosításához a `+=` és a `-=` operátorokat használhatja: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // növeli a "pontok" oszlop értékét 1 ponttal. + 'coins-=' => 1, // csökkenti az "érmék" oszlop értékét 1-gyel. ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Törlés (a törölt sorok számát adja vissza): + +Selection::delete(): int .[method] +---------------------------------- + +Töröl sorokat egy táblázatból egy megadott szűrő alapján. Visszaadja a törölt sorok számát. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +A `update()` vagy a `delete()` meghívásakor mindenképpen használja a `where()` címet a frissítendő vagy törlendő sorok megadásához. Ha nem használja a `where()` címet, a művelet a teljes táblán fog végrehajtódni! + -Kapcsolatokkal való munka .[#toc-working-with-relationships] -============================================================ +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Frissíti a `ActiveRow` objektum által reprezentált adatbázis-sor adatait. Paraméterként iterálható adatokat fogad el, ahol a kulcsok oszlopnevek. A numerikus értékek módosításához a `+=` és a `-=` operátorokat használhatja: -Van egy kapcsolata .[#toc-has-one-relation] -------------------------------------------- -A Has one reláció egy gyakori felhasználási eset. A könyvnek *egy* szerzője van. A könyvnek *egy* fordítója van. A kapcsolódó sorok kinyerése főként a `ref()` módszerrel történik. Két argumentumot fogad el: a céltábla nevét és a forrás összekötő oszlopát. Lásd a példát: +A frissítés végrehajtása után a `ActiveRow` objektum automatikusan újratöltődik az adatbázisból, hogy tükrözze az adatbázis szintjén (pl. triggerek) végrehajtott változásokat. A módszer csak akkor adja vissza a `true` értéket, ha valódi adatváltozás történt. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // növeli a megtekintések számát +]); +echo $article->views; // Kimeneti az aktuális nézetszámot ``` -A fenti példában a `author` táblából hozzuk le a kapcsolódó szerzői bejegyzést, a szerző elsődleges kulcsát a `book.author_id` oszlop segítségével keressük. A Ref() metódus visszaadja az ActiveRow példányt vagy nullát, ha nincs megfelelő bejegyzés. A visszaadott sor az ActiveRow egy példánya, így ugyanúgy dolgozhatunk vele, mint a könyv bejegyzéssel. +Ez a módszer csak egy adott sort frissít az adatbázisban. Több sor tömeges frissítéséhez használja a [Selection::update() |#Selection::update()] metódust. + + +ActiveRow::delete() .[method] +----------------------------- + +Töröl egy sort az adatbázisból, amelyet a `ActiveRow` objektum képvisel. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Törli az 1 azonosítóval rendelkező könyvet +``` + +Ez a módszer csak egy adott sort töröl az adatbázisból. Több sor tömeges törléséhez használja a [Selection::delete() |#Selection::delete()] metódust. + -// vagy közvetlenül -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Táblák közötti kapcsolatok .[#toc-relationships-between-tables] +=============================================================== + +A relációs adatbázisokban az adatok több táblára vannak felosztva, és idegen kulcsok segítségével kapcsolódnak egymáshoz. A Nette Database Explorer forradalmi módot kínál az ilyen kapcsolatokkal való munkára - JOIN-lekérdezések írása nélkül, illetve anélkül, hogy bármilyen konfigurációt vagy entitásgenerálást igényelne. + +A bemutatáshoz a **példaadatbázist** fogjuk használni[(elérhető a GitHubon |https://github.com/nette-examples/books]). Az adatbázis a következő táblákat tartalmazza: + +- `author` - szerzők és fordítók ( `id`, `name`, `web`, `born` oszlopok). +- `book` - könyvek (oszlopok: `id`, `author_id`, `translator_id`, `title`, `sequel_id`). +- `tag` - címkék ( `id`, `name` oszlopok). +- `book_tag` - könyvek és címkék közötti kapcsolati táblázat ( `book_id`, `tag_id` oszlopok). + +[* db-schema-1-.webp *] *** Adatbázis szerkezete .<> + +Ebben a könyvadatbázis példában többféle kapcsolatot találunk (a valósághoz képest leegyszerűsítve): + +- **Egytől sokig (1:N)** - Minden könyvnek **egy** szerzője van; egy szerző **több** könyvet is írhat. +- **Nulla a sokhoz (0:N)** - Egy könyvnek **lehet** egy fordítója; egy fordító **több** könyvet is lefordíthat. +- **Nulla az egyhez (0:1)** - Egy könyvnek **lehet** folytatása. +- **Sok-sok (M:N)** - Egy könyvnek **több** címkéje is lehet, és egy címke **több** könyvhöz is hozzárendelhető. + +Ezekben a kapcsolatokban mindig van egy **szülő tábla** és egy **gyermek tábla**. Például a szerzők és a könyvek közötti kapcsolatban a `author` tábla a szülő, a `book` tábla pedig a gyermek - úgy is elképzelhető, hogy egy könyv mindig egy szerzőhöz "tartozik". Ez az adatbázis szerkezetében is tükröződik: a `book` gyermek tábla tartalmazza a `author_id` idegen kulcsot, amely a `author` szülő táblára hivatkozik. + +Ha a könyveket a szerzők nevével együtt szeretnénk megjeleníteni, két lehetőségünk van. Vagy egyetlen SQL-lekérdezéssel lekérdezzük az adatokat JOIN-nal: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -A könyvnek is van egy fordítója, így a fordító nevének megszerzése elég egyszerű. +Vagy két lépésben - először a könyveket, majd a szerzőket - és PHP-ben állítjuk össze az adatokat: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books +``` + +A második megközelítés meglepő módon **hatékonyabb**. Az adatokat csak egyszer hívjuk le, és a gyorsítótárban jobban kihasználhatók. A Nette Database Explorer pontosan így működik - mindent a motorháztető alatt kezel, és tiszta API-t biztosít: + ```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 egy rekord a 'author' táblából + echo 'translated by: ' . $book->translator?->name; +} ``` -Mindez rendben van, de kissé nehézkes, nem gondolja? Az adatbázis-kutató már tartalmazza az idegen kulcsok definícióit, miért ne használhatná őket automatikusan? Csináljuk meg! -Ha olyan tulajdonságot hívunk meg, amely nem létezik, az ActiveRow megpróbálja a hívó tulajdonság nevét 'van egy' relációként feloldani. Ennek a tulajdonságnak a megszerzése ugyanaz, mint a ref() metódus hívása egyetlen argumentummal. Az egyetlen argumentumot **key**-nek fogjuk hívni. A kulcsot egy adott idegen kulcs relációra fogjuk felbontani. Az átadott kulcsot összevetjük a sor oszlopokkal, és ha egyezik, akkor a megfelelő oszlopon definiált idegen kulcsot használjuk a kapcsolódó céltáblából való adatszerzéshez. Lásd a példát: +Hozzáférés a szülői táblázathoz .[#toc-accessing-the-parent-table] +------------------------------------------------------------------ + +Az anyatáblához való hozzáférés egyszerű. Ezek olyan kapcsolatok, mint például *egy könyvnek van szerzője* vagy *egy könyvnek lehet fordítója*. A kapcsolódó rekordot a `ActiveRow` objektumtulajdonságon keresztül lehet elérni - a tulajdonság neve megegyezik az idegen kulcs oszlopnevével a `id` utótag nélkül: ```php -$book->author->name; -// ugyanaz, mint -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // megtalálja a szerzőt az 'author_id' oszlopon keresztül +echo $book->translator?->name; // a fordítót a 'translator_id' oszlopon keresztül találja meg. ``` -Az ActiveRow példánynak nincs szerző oszlopa. Az összes könyv oszlopban keresünk egyezést a *kulcs*-val. Az egyezés ebben az esetben azt jelenti, hogy az oszlop nevének tartalmaznia kell a kulcsot. A fenti példában tehát a `author_id` oszlop tartalmazza a 'szerző' karakterláncot, ezért a keresés a 'szerző' kulccsal történik. Ha a könyv fordítóját szeretné megkapni, akkor kulcsként használhatja pl. a 'translator' szót, mert a 'translator' kulcs a `translator_id` oszlopra fog illeszkedni. A kulcsillesztési logikáról bővebben a [Joining expressions |#joining-key] fejezetben olvashat. +A `$book->author` tulajdonság elérésekor az Explorer a `book` táblában olyan oszlopot keres, amely a `author` karakterláncot tartalmazza (pl. `author_id`). Az ebben az oszlopban lévő érték alapján lekérdezi a megfelelő rekordot a `author` táblából, és azt `ActiveRow` objektumként adja vissza. Hasonlóképpen, a `$book->translator` a `translator_id` oszlopot használja. Mivel a `translator_id` oszlop tartalmazhatja a `null` oszlopot, a `?->` operátort használja. + +Alternatív megközelítést kínál a `ref()` metódus, amely két argumentumot fogad el - a céltábla nevét és az összekötő oszlopot -, és egy `ActiveRow` példányt vagy a `null`-t adja vissza: ```php -echo $book->title . ': '; -echo $book->author->name; -if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; -} +echo $book->ref('author', 'author_id')->name; // link a szerzőhöz +echo $book->ref('author', 'translator_id')->name; // link a fordítóhoz ``` -Ha több könyvet szeretne lekérdezni, ugyanezt a megközelítést kell alkalmaznia. A Nette Database Explorer egyszerre fogja lekérdezni az összes lekérdezett könyv szerzőit és fordítóit. +A `ref()` módszer akkor hasznos, ha a tulajdonságalapú hozzáférés nem használható, például ha a táblázat tartalmaz egy olyan oszlopot, amelynek neve megegyezik a tulajdonsággal (`author`). Más esetekben a jobb olvashatóság érdekében ajánlott a tulajdonságalapú hozzáférés használata. + +Az Explorer automatikusan optimalizálja az adatbázis-lekérdezéseket. A könyvek iterálásakor és a kapcsolódó rekordok (szerzők, fordítók) elérésekor az Explorer nem generál lekérdezést minden egyes könyvre külön-külön. Ehelyett csak **egy SELECT lekérdezést hajt végre minden egyes kapcsolattípushoz**, jelentősen csökkentve ezzel az adatbázis terhelését. Például: ```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; } ``` -A kód csak ezt a 3 lekérdezést fogja lefuttatni: +Ez a kód csak három optimalizált adatbázis-lekérdezést hajt végre: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Az összekötő oszlop azonosításának logikáját a [Conventions |api:Nette\Database\Conventions] implementációja határozza meg. Javasoljuk a [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions] használatát, amely elemzi az idegen kulcsokat, és lehetővé teszi, hogy zökkenőmentesen dolgozzon a meglévő táblázati kapcsolatokkal. + -Has Many Relation .[#toc-has-many-relation] -------------------------------------------- +Hozzáférés a gyermektáblához .[#toc-accessing-the-child-table] +-------------------------------------------------------------- -A 'Has many' reláció a 'has one' reláció fordítottja. A szerző *már* sok* könyvet írt. A szerző *mindegyik* könyvet lefordította. Amint láthatjuk, ez a fajta reláció egy kicsit nehezebb, mivel a reláció 'megnevezett' ('írt', 'fordított'). Az ActiveRow példány rendelkezik a `related()` metódussal, amely a kapcsolódó bejegyzések tömbjét adja vissza. A bejegyzések szintén ActiveRow példányok. Lásd az alábbi példát: +A gyermektáblához való hozzáférés ellenkező irányban működik. Most azt kérdezzük, hogy *melyik könyvet írta ez a szerző* vagy *melyik könyvet fordította ez a fordító*. Az ilyen típusú lekérdezéshez a `related()` metódust használjuk, amely egy `Selection` objektumot ad vissza a kapcsolódó rekordokkal. Íme egy példa: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// A szerző által írt összes könyv kimenete foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// A szerző által lefordított összes könyv kiadása foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -A `related()` módszer elfogadja a két argumentumként vagy egy argumentumként, ponttal összekötve átadott teljes join leírást. Az első argumentum a céltábla, a második a céloszlop. +A `related()` módszer a kapcsolat leírását egyetlen argumentumként fogadja el, pontjelöléssel vagy két külön argumentumként: + +```php +$author->related('book.translator_id'); // egyetlen érv +$author->related('book', 'translator_id'); // két érv +``` + +Az Explorer automatikusan felismeri a megfelelő kapcsoló oszlopot a szülő tábla neve alapján. Ebben az esetben a `book.author_id` oszlopon keresztül linkel, mivel a forrás tábla neve `author`: ```php -$author->related('book.translator_id'); -// ugyanaz, mint -$author->related('book', 'translator_id'); +$author->related('book'); // használ book.author_id ``` -Használhatja a Nette Database Explorer idegen kulcsokon alapuló heurisztikáját, és csak a **key** argumentumot adhatja meg. A kulcsot az aktuális táblára (`author` tábla) mutató összes idegen kulccsal összeveti a rendszer. Ha van egyezés, a Nette Database Explorer ezt az idegen kulcsot fogja használni, ellenkező esetben [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] vagy [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] hibát fog dobni. A kulcsillesztési logikáról bővebben a [Joining expressions |#joining-key] fejezetben olvashat. +Ha több lehetséges kapcsolat létezik, az Explorer [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] kivételt dob. -Természetesen az összes lehívott szerzőhöz meghívhatja a kapcsolódó metódusokat, a Nette Database Explorer ismét egyszerre fogja lehívni a megfelelő könyveket. +Természetesen használhatjuk a `related()` módszert akkor is, ha több rekordon iterálunk egy ciklusban, és az Explorer ebben az esetben is automatikusan optimalizálja a lekérdezéseket: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -A fenti példa csak két lekérdezést futtat: +Ez a kód csak két hatékony SQL-lekérdezést generál: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Az Explorer manuális létrehozása .[#toc-creating-explorer-manually] -=================================================================== +Sok-sok közötti kapcsolat .[#toc-many-to-many-relationship] +----------------------------------------------------------- + +A sok-sok (M:N) kapcsolathoz egy **összekötő táblára** (esetünkben a `book_tag`) van szükség. Ez a tábla két idegen kulcs oszlopot tartalmaz (`book_id`, `tag_id`). Mindkét oszlop a kapcsolt táblák egyikének elsődleges kulcsára hivatkozik. A kapcsolódó adatok lekérdezéséhez először a `related('book_tag')` segítségével a kapcsoló táblából hívjuk le a rekordokat, majd folytatjuk a céladatokkal: + +```php +$book = $explorer->table('book')->get(1); +// A könyvhöz rendelt címkék neveinek kiadása +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // a címke nevét a link táblán keresztül szerzi be +} + +$tag = $explorer->table('tag')->get(1); +// Ellenkező irányban: az adott címkével rendelkező könyvek címeit adja ki. +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // a könyv címének lekérdezése +} +``` + +Az Explorer ismét hatékony formára optimalizálja az SQL-lekérdezéseket: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Kapcsolódó táblákon keresztül történő lekérdezés .[#toc-querying-through-related-tables] +---------------------------------------------------------------------------------------- + +A `where()`, `select()`, `order()` és `group()` metódusokban speciális jelöléseket használhat más táblák oszlopainak eléréséhez. Az Explorer automatikusan létrehozza a szükséges JOIN-okat. + +A **Pont jelölés** (`parent_table.column`) a szülő tábla szemszögéből nézve az 1:N kapcsolatokhoz használatos: + +```php +$books = $explorer->table('book'); + +// Megkeresi azokat a könyveket, amelyek szerzőinek neve 'Jon' betűvel kezdődik. +$books->where('author.name LIKE ?', 'Jon%'); + +// A könyveket a szerző neve szerint rendezi csökkenő sorrendbe +$books->order('author.name DESC'); + +// A könyv címe és a szerző neve +$books->select('book.title, author.name'); +``` + +**Kettőspont jelölés** a szülő tábla szemszögéből nézve az 1:N kapcsolatokra használatos: + +```php +$authors = $explorer->table('author'); + +// Megkeresi azokat a szerzőket, akik olyan könyvet írtak, amelynek a címében szerepel a 'PHP' szó +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Megszámolja az egyes szerzők könyveinek számát +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +A fenti példában a kettőspont jelöléssel (`:book.title`) az idegen kulcs oszlop nincs kifejezetten megadva. Az Explorer automatikusan felismeri a megfelelő oszlopot a szülő tábla neve alapján. Ebben az esetben a `book.author_id` oszlopon keresztül csatlakozik, mivel a forrás tábla neve `author`. Ha több lehetséges kapcsolat létezik, az Explorer az [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] kivételt dobja. + +Az összekötő oszlop kifejezetten megadható zárójelben: + +```php +// Találja meg azokat a szerzőket, akik olyan könyvet fordítottak, amelynek a címében szerepel a 'PHP' szó +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +A jelölések láncolhatók, hogy több táblában is hozzáférjenek az adatokhoz: + +```php +// 'PHP' címkével ellátott könyvek szerzőinek keresése +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + -Az adatbázis-kapcsolat az alkalmazás konfigurációjának segítségével hozható létre. Ilyenkor létrejön egy `Nette\Database\Explorer` szolgáltatás, amelyet függőségként át lehet adni a DI konténer segítségével. +A JOIN feltételeinek kiterjesztése .[#toc-extending-conditions-for-join] +------------------------------------------------------------------------ -Ha azonban a Nette Database Explorer önálló eszközként használatos, akkor a `Nette\Database\Explorer` objektum példányát kézzel kell létrehozni. +A `joinWhere()` módszer a `ON` kulcsszó után további feltételeket ad az SQL-ben a táblázatok összekapcsolásához. + +Tegyük fel például, hogy egy adott fordító által lefordított könyveket szeretnénk megtalálni: + +```php +// Megtalálja a 'David' nevű fordító által lefordított könyveket. +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +A `joinWhere()` feltételben ugyanazokat a konstrukciókat használhatja, mint a `where()` módszerben - operátorokat, helyőrzőket, értéktömböket vagy SQL-kifejezéseket. + +Összetettebb, több JOIN-t tartalmazó lekérdezésekhez táblázat aliasokat lehet definiálni: ```php -// $storage implementálja a Nette\Caching\Storage: -$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); +$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) +``` + +Vegye figyelembe, hogy míg a `where()` módszer a `WHERE` záradékhoz feltételeket ad hozzá, addig a `joinWhere()` módszer a `ON` záradékban szereplő feltételeket bővíti ki a táblaösszekötések során. + + +Az Explorer manuális létrehozása .[#toc-manually-creating-explorer] +=================================================================== + +Ha nem a Nette DI konténert használja, akkor manuálisan is létrehozhatja a `Nette\Database\Explorer` egy példányát: + +```php +use Nette\Database; + +// $storage megvalósítja a Nette\Caching\Storage-t, pl.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// adatbázis-kapcsolat +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// kezeli az adatbázis struktúrájának tükrözését +$structure = new Database\Structure($connection, $storage); +// meghatározza a táblák neveinek, oszlopainak és idegen kulcsainak leképezésére vonatkozó szabályokat. +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/hu/security.texy b/database/hu/security.texy new file mode 100644 index 0000000000..9cb28cf27c --- /dev/null +++ b/database/hu/security.texy @@ -0,0 +1,145 @@ +Biztonsági kockázatok +********************* + +.[perex] +Az adatbázisok gyakran tartalmaznak érzékeny adatokat, és veszélyes műveleteket tesznek lehetővé. A Nette Database számos biztonsági funkciót biztosít. Létfontosságú azonban, hogy megértsük a biztonságos és a nem biztonságos API-k közötti különbséget. + + +SQL injekció .[#toc-sql-injection] +================================== + +Az SQL injektálás a legsúlyosabb biztonsági kockázat az adatbázisokkal való munka során. Akkor fordul elő, amikor a nem ellenőrzött felhasználói bemenet egy SQL-lekérdezés részévé válik. A támadó saját SQL-parancsokat injektálhat, így adatokat szerezhet vagy módosíthat az adatbázisban. + +```php +// ❌ UNSAFE CODE - sebezhető SQL injekcióval szemben +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// A támadó valami ilyesmit adhat be: ' OR '1'='1 +// Az eredményül kapott lekérdezés a következő lesz: +// Vagy '1'='1': SELECT * FROM users WHERE name = '' OR '1'='1' +// Ez az összes felhasználót visszaadja! +``` + +Ugyanez vonatkozik a Database Explorerre is: + +```php +// ❌ UNSAFE CODE +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Biztonságos paraméterezett lekérdezések .[#toc-safe-parameterized-queries] +========================================================================== + +Az értékek SQL-lekérdezésekbe történő beszúrásának biztonságos módja a paraméterezett lekérdezések. A Nette Database többféle lehetőséget biztosít ezek használatára. + + +Helyőrző kérdőjelek .[#toc-placeholder-question-marks] +------------------------------------------------------ + +A legegyszerűbb módszer a helyőrző kérdőjelek használata: + +```php +// ✅ Biztonságos paraméterezett lekérdezések +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Biztonságos feltétel az Explorerben +$table->where('name = ?', $_GET['name']); +``` + +Ugyanez vonatkozik az Adatbázis-kutató összes többi módszerére, amelyek lehetővé teszik a helyőrző kérdőjelekkel és paraméterekkel ellátott kifejezések beillesztését. + +.[warning] +Az értékeknek skalár típusúaknak kell lenniük (`string`, `int`, `float`, `bool`) vagy `null`. Ha például, `$_GET['name']` egy tömb, a Nette Database annak minden elemét be fogja vonni az SQL-lekérdezésbe, ami nem kívánatos lehet. + + +Értéktáblák .[#toc-value-arrays] +-------------------------------- + +A `INSERT`, `UPDATE` vagy `WHERE` záradékok esetében használhatunk értéktömböket: + +```php +// ✅ Biztonságos BESZÜNTETÉS +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Biztonságos UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +A Nette Database automatikusan megszünteti a paraméteres lekérdezéseken keresztül átadott értékeket. Biztosítanunk kell azonban a paraméterek helyes adattípusát. + + +A tömbkulcsok nem biztonságos API .[#toc-array-keys-are-not-a-safe-api] +======================================================================= + +Míg a tömbök értékei biztonságosak, ugyanez nem mondható el a kulcsokról: + +```php +// ❌ UNSAFE CODE - a kulcsok SQL injekciót tartalmazhatnak +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +A `INSERT` és a `UPDATE` parancsok esetében ez kritikus biztonsági hiba - egy támadó bármelyik oszlopot beszúrhatja vagy módosíthatja az adatbázisban. Például beállíthatja a `is_admin = 1` címet, vagy tetszőleges adatokat illeszthet be érzékeny oszlopokba. + +A `WHERE` feltételek esetén ez még veszélyesebb, mert lehetővé teszi a **SQL enumerációt** - egy olyan technikát, amellyel fokozatosan lehet információt szerezni az adatbázisról. Egy támadó megkísérelhetné feltárni a dolgozói fizetéseket a `$_GET` beillesztésével, például így: + +```php +$_GET = ['salary >', 100000]; // megkezdi a fizetési sávok meghatározását +``` + +A fő probléma azonban az, hogy a `WHERE` feltételek támogatják az SQL-kifejezéseket a kulcsokban: + +```php +// Az operátorok jogszerű használata a kulcsokban +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ UNSAFE: a támadó saját SQL-t tud beadni +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // lehetővé teszi a támadó számára az adminisztrátori fizetések megszerzését +``` + +Ez ismét **SQL injekció**. + + +Az oszlopok fehér listázása .[#toc-whitelisting-columns] +-------------------------------------------------------- + +Ha engedélyezni szeretné, hogy a felhasználók oszlopokat választhassanak, mindig használjon fehérlistát: + +```php +// ✅ Biztonságos feldolgozás - csak engedélyezett oszlopok +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dinamikus azonosítók .[#toc-dynamic-identifiers] +================================================ + +A dinamikus tábla- és oszlopnevekhez használja a `?name` helyőrzőt: + +```php +// ✅ Megbízható azonosítók biztonságos használata +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - soha ne használjon felhasználói bemenetet +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +A `?name` szimbólumot csak az alkalmazáskódban meghatározott megbízható értékek esetén szabad használni. A felhasználó által megadott értékekhez ismét használjon fehérlistát. diff --git a/database/it/@left-menu.texy b/database/it/@left-menu.texy index ea5f41a8db..f8a2b499dc 100644 --- a/database/it/@left-menu.texy +++ b/database/it/@left-menu.texy @@ -4,3 +4,4 @@ Database - [Esploratore |Explorer] - [Riflessione |Reflection] - [Configurazione |Configuration] +- [Rischi per la sicurezza |security] diff --git a/database/it/explorer.texy b/database/it/explorer.texy index 431a45ae78..16bcadae8c 100644 --- a/database/it/explorer.texy +++ b/database/it/explorer.texy @@ -3,548 +3,927 @@ Esploratore di database
-Nette Database Explorer semplifica notevolmente il recupero dei dati dal database senza dover scrivere query SQL. +Nette Database Explorer è un potente livello che semplifica notevolmente il recupero dei dati dal database senza la necessità di scrivere query SQL. -- utilizza query efficienti -- non trasmette dati inutilmente -- presenta una sintassi elegante +- Lavorare con i dati è naturale e facile da capire +- Genera query SQL ottimizzate che recuperano solo i dati necessari +- Fornisce un facile accesso a dati correlati senza la necessità di scrivere query JOIN +- Funziona immediatamente senza alcuna configurazione o generazione di entità
-Per utilizzare Database Explorer, iniziare con una tabella - chiamare `table()` su un oggetto [api:Nette\Database\Explorer]. Il modo più semplice per ottenere un'istanza dell'oggetto contesto è [descritto qui |core#Connection and Configuration], oppure, nel caso in cui Nette Database Explorer venga utilizzato come strumento autonomo, è possibile [crearlo manualmente |#Creating Explorer Manually]. +Nette Database Explorer è un'estensione del livello di basso livello di [Nette Database Core |core], che aggiunge un comodo approccio orientato agli oggetti alla gestione dei database. + +Il lavoro con Explorer inizia chiamando il metodo `table()` sull'oggetto [api:Nette\Database\Explorer] (come ottenerlo è [descritto qui |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // il nome della tabella del db è 'libro' +$books = $explorer->table('book'); // 'book' è il nome della tabella ``` -La chiamata restituisce un'istanza dell'oggetto [Selection |api:Nette\Database\Table\Selection], che può essere iterata per recuperare tutti i libri. Ogni elemento (una riga) è rappresentato da un'istanza di [ActiveRow |api:Nette\Database\Table\ActiveRow] con i dati mappati sulle sue proprietà: +Il metodo restituisce un oggetto [Selection |api:Nette\Database\Table\Selection], che rappresenta una query SQL. È possibile concatenare altri metodi a questo oggetto per filtrare e ordinare i risultati. La query viene assemblata ed eseguita solo quando i dati vengono richiesti, ad esempio iterando con `foreach`. Ogni riga è rappresentata da un oggetto [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // produce la colonna "titolo + echo $book->author_id; // produce la colonna "author_id } ``` -Per ottenere una riga specifica si utilizza il metodo `get()`, che restituisce direttamente un'istanza di ActiveRow. +Explorer semplifica notevolmente il lavoro con le [relazioni tra le tabelle |#Vazby mezi tabulkami]. L'esempio seguente mostra la facilità con cui si possono ottenere dati da tabelle correlate (libri e relativi autori). Si noti che non è necessario scrivere query JOIN: Nette le genera per noi: ```php -$book = $explorer->table('book')->get(2); // restituisce il libro con id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // crea una JOIN con la tabella "autore". +} ``` -Vediamo un caso d'uso comune. È necessario recuperare i libri e i loro autori. Si tratta di una comune relazione 1:N. La soluzione spesso utilizzata è quella di recuperare i dati utilizzando un'unica query SQL con join di tabelle. La seconda possibilità è recuperare i dati separatamente, eseguire una query per ottenere i libri e poi ottenere un autore per ogni libro con un'altra query (ad esempio nel ciclo foreach). Questo potrebbe essere facilmente ottimizzato per eseguire solo due query, una per i libri e un'altra per gli autori necessari, e questo è esattamente il modo in cui Nette Database Explorer lo fa. +Nette Database Explorer ottimizza le query per ottenere la massima efficienza. L'esempio precedente esegue solo due query SELECT, indipendentemente dal fatto che vengano elaborati 10 o 10.000 libri. -Negli esempi che seguono, lavoreremo con lo schema di database riportato in figura. Ci sono collegamenti OneHasMany (1:N) (autore del libro `author_id` e possibile traduttore `translator_id`, che può essere `null`) e ManyHasMany (M:N) tra il libro e i suoi tag. +Inoltre, Explorer tiene traccia delle colonne utilizzate nel codice e recupera solo quelle dal database, risparmiando ulteriori prestazioni. Questo comportamento è completamente automatico e adattivo. Se in seguito si modifica il codice per utilizzare altre colonne, Explorer adatta automaticamente le query. Non è necessario configurare nulla o pensare a quali colonne saranno necessarie: questo compito spetta a Nette. -[Un esempio, comprensivo di schema, si trova su GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Struttura del database utilizzata negli esempi .<> +Filtraggio e ordinamento .[#toc-filtering-and-sorting] +====================================================== -Il codice seguente elenca il nome dell'autore per ogni libro e tutti i suoi tag. Tra poco [vedremo |#Working with relationships] come funziona internamente. +La classe `Selection` fornisce metodi per filtrare e ordinare i dati. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Aggiunge una condizione WHERE. Più condizioni sono combinate con AND | +| `whereOr(array $conditions)` | Aggiunge un gruppo di condizioni WHERE combinate usando OR | +| `wherePrimary($value)` | Aggiunge una condizione WHERE basata sulla chiave primaria | +| `order($columns, ...$params)` | Imposta l'ordinamento con ORDER BY | +| `select($columns, ...$params)` | Specifica quali colonne recuperare | +| `limit($limit, $offset = null)` | Limita il numero di righe (LIMIT) e imposta facoltativamente OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Imposta la paginazione | +| `group($columns, ...$params)` | Raggruppa le righe (GROUP BY) | +| `having($condition, ...$params)`| Aggiunge una condizione HAVING per filtrare le righe raggruppate | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->autore è una riga della tabella 'autore'. +I metodi possono essere concatenati (la cosiddetta [interfaccia fluente |nette:introduction-to-object-oriented-programming#fluent-interfaces]): `$table->where(...)->order(...)->limit(...)`. - echo 'tag: '; - foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name . ', '; // $bookTag->tag è una riga della tabella 'tag'. - } -} -``` +Questi metodi consentono anche di utilizzare notazioni speciali per accedere ai [dati di tabelle correlate |#Dotazování přes související tabulky]. -Si può notare l'efficienza con cui funziona il livello di database. L'esempio precedente effettua un numero costante di richieste che assomigliano a queste: -```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)) -``` +Escaping e identificatori .[#toc-escaping-and-identifiers] +---------------------------------------------------------- -Se si utilizza la [cache |caching:] (per impostazione predefinita), nessuna colonna verrà interrogata inutilmente. Dopo la prima query, la cache memorizzerà i nomi delle colonne utilizzate e Nette Database Explorer eseguirà le query solo con le colonne necessarie: +I metodi eseguono automaticamente l'escape dei parametri e degli identificatori di quote (nomi di tabelle e colonne), impedendo l'iniezione di SQL. Per garantire un funzionamento corretto, è necessario seguire alcune regole: -```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)) +- Scrivere parole chiave, nomi di funzioni, procedure, ecc. in **maiuscolo**. +- Scrivere i nomi delle colonne e delle tabelle in **minuscolo**. +- Passare sempre stringhe utilizzando **parametri**. + +```php +where('name = ' . $name); // **DISASTRO**: vulnerabile all'iniezione SQL +where('name LIKE "%search%"'); // **VERSO**: complica il quoting automatico +where('name LIKE ?', '%search%'); // **CORRETTO**: valore passato come parametro + +where('name like ?', $name); // **WRONG**: genera: `nome` `come` ? +where('name LIKE ?', $name); // **CORRETTO**: genera: `nome` LIKE ? +where('LOWER(name) = ?', $value);// **CORRETTO**: LOWER(`nome`) = ? ``` -Selezioni .[#toc-selections] -============================ +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Vedere le possibilità di filtrare e limitare le righe [api:Nette\Database\Table\Selection]: +Filtra i risultati utilizzando le condizioni WHERE. Il suo punto di forza è la gestione intelligente di vari tipi di valori e la selezione automatica degli operatori SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Impostare WHERE utilizzando AND come collante se vengono fornite due o più condizioni -| `$table->whereOr($where)` | Impostare WHERE usando OR come collante se vengono fornite due o più condizioni -| `$table->order($columns)` | Impostare ORDER BY, può essere un'espressione `('column DESC, id DESC')` -| `$table->select($columns)` | Impostare le colonne recuperate, può essere un'espressione `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Impostare LIMIT e OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Abilita la paginazione -| `$table->group($columns)` | Impostare GROUP BY -| `$table->having($having)` | Imposta HAVING +Utilizzo di base: -Possiamo utilizzare una cosiddetta [interfaccia fluente |nette:introduction-to-object-oriented-programming#fluent-interfaces], ad esempio `$table->where(...)->order(...)->limit(...)`. Più condizioni `where` o `whereOr` sono collegate dall'operatore `AND`. +```php +$table->where('id', $value); // DOVE `id` = 123 +$table->where('id > ?', $value); // DOVE `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // DOVE `id` = 1 OR `nome` = 'Jon Snow' +``` +Grazie al rilevamento automatico degli operatori adatti, non è necessario gestire casi particolari: Nette li gestisce per voi: -dove() .[#toc-where] --------------------- +```php +$table->where('id', 1); // DOVE `id` = 1 +$table->where('id', null); // DOVE `id` È NULL +$table->where('id', [1, 2, 3]); // DOVE `id` IN (1, 2, 3) +// Il segnaposto ? può essere utilizzato senza operatore: +$table->where('id ?', 1); // DOVE `id` = 1 +``` -Nette Database Explorer può aggiungere automaticamente gli operatori necessari per i valori passati: +Il metodo gestisce correttamente anche le condizioni negative e gli array vuoti: -.[language-php] -| `$table->where('field', $value)` | campo = $valore -| `$table->where('field', null)` | campo IS NULL -| `$table->where('field > ?', $val)` | campo > $val -| `$table->where('field', [1, 2])` | campo IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OR nome = "Jon Snow -| `$table->where('field', $explorer->table($tableName))` | campo IN (SELECT $primario FROM $nometabella) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | campo IN (SELECT col FROM $tableName) +```php +$table->where('id', []); // DOVE `id` È NULLO E FALSO -- non trova niente +$table->where('id NOT', []); // DOVE `id` È NULLO O VERO -- trova tutto +$table->where('NOT (id ?)', []); // DOVE NON (`id` È NULLO E FALSO) -- trova tutto. +// $table->where('NOT id ?', $ids); // ATTENZIONE: questa sintassi non è supportata +``` -È possibile fornire un segnaposto anche senza l'operatore di colonna. Queste chiamate sono identiche. +È inoltre possibile passare come parametro il risultato di un'altra query di tabella, creando una sottoquery: ```php -$table->where('id = ? OR id = ?', 1, 2); -$table->where('id ? OR id ?', 1, 2); +// DOVE `id` IN (SELECT `id` FROM `tableName`) +$table->where('id', $explorer->table($tableName)); + +// DOVE `id` IN (SELECT `col` FROM `tableName`) +$table->where('id', $explorer->table($tableName)->select('col')); ``` -Questa funzione consente di generare l'operatore corretto in base al valore: +Le condizioni possono essere passate anche come array, con gli elementi combinati tramite AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// DOVE (`prezzo_finale` < `prezzo_originale`) E (`conteggio scorte` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -La selezione gestisce correttamente anche le condizioni negative, funziona anche per gli array vuoti: +Nell'array si possono usare coppie chiave-valore e Nette sceglierà automaticamente gli operatori corretti: ```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) +// DOVE (`status` = 'attivo') E (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` -// questo lancerà un'eccezione, questa sintassi non è supportata -$table->where('NOT id ?', $ids); +Possiamo anche mescolare espressioni SQL con segnaposto e parametri multipli. Questo è utile per condizioni complesse con operatori definiti con precisione: + +```php +// DOVE (`età` > 18) E (ROUND(`punteggio`, 2) > 75,5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // due parametri vengono passati come array +]); ``` +Chiamate multiple a `where()` combinano automaticamente le condizioni usando AND. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Esempio di utilizzo senza parametri: +Simile a `where()`, ma combina le condizioni utilizzando OR: ```php -// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`)) +// DOVE (`status` = 'attivo') OPPURE (`cancellato` = 1) $table->whereOr([ - 'user_id IS NULL', - 'SUM(field1) > SUM(field2)', + 'status' => 'active', + 'deleted' => true, ]); ``` -Utilizziamo i parametri. Se non si specifica un operatore, Nette Database Explorer aggiungerà automaticamente quello appropriato: +È possibile utilizzare anche espressioni più complesse: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// DOVE (`prezzo` > 1000) O (`prezzo_con_tassa` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -La chiave può contenere un'espressione contenente punti interrogativi jolly e poi passare i parametri nel valore: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Aggiunge una condizione per la chiave primaria della tabella: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// DOVE `id` = 123 +$table->wherePrimary(123); + +// DOVE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Se la tabella ha una chiave primaria composta (ad esempio, `foo_id`, `bar_id`), la si passa come array: + +```php +// DOVE `foo_id` = 1 E `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); + +// DOVE (`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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Esempi di utilizzo: +Specifica l'ordine in cui vengono restituite le righe. È possibile ordinare in base a una o più colonne, in ordine crescente o decrescente, oppure in base a un'espressione personalizzata: ```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'); // ORDINE PER `creato` +$table->order('created DESC'); // ORDINE PER `creato` DESC +$table->order('priority DESC, created'); // ORDINATO PER `priorità' DESC, `creato' +$table->order('status = ? DESC', 'active'); // ORDINATO PER `status` = 'attivo' DESC ``` -select() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Esempi di utilizzo: +Specifica le colonne da restituire dal database. Per impostazione predefinita, Nette Database Explorer restituisce solo le colonne effettivamente utilizzate nel codice. Utilizzate il metodo `select()` quando dovete recuperare espressioni specifiche: ```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) +// SELEZIONA *, DATE_FORMAT(`created_at`, "%d.%m.%Y") come `formatted_date`. +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Gli alias definiti con `AS` sono accessibili come proprietà dell'oggetto `ActiveRow`: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // accedere all'alias +} +``` -limit() .[#toc-limit] ---------------------- -Esempi di utilizzo: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Limita il numero di righe restituite (LIMIT) e imposta facoltativamente un offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (restituisce le prime 10 righe) +$table->limit(10, 20); // LIMITE 10 OFFSET 20 ``` +Per la paginazione, è più appropriato utilizzare il metodo `page()`. + -page() .[#toc-page] -------------------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Un modo alternativo per impostare il limite e l'offset: +Semplifica la paginazione dei risultati. Accetta il numero di pagina (a partire da 1) e il numero di elementi per pagina. Opzionalmente, è possibile passare un riferimento a una variabile in cui verrà memorizzato il numero totale di pagine: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Ottenere il numero dell'ultima pagina, passato alla variabile `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Raggruppa le righe in base alle colonne specificate (GROUP BY). Si usa in genere in combinazione con le funzioni di aggregazione: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Conta il numero di prodotti in ogni categoria +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Esempi di utilizzo: +Imposta una condizione per filtrare le righe raggruppate (HAVING). Può essere utilizzata in combinazione con il metodo `group()` e le funzioni di aggregazione: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Trova le categorie con più di 100 prodotti +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -avere() .[#toc-having] ----------------------- +Dati di lettura +=============== + +Per leggere i dati dal database sono disponibili diversi metodi utili: + +.[language-php] +| `foreach ($table as $key => $row)` | Itera tutte le righe, `$key` è il valore della chiave primaria, `$row` è un oggetto ActiveRow | +| `$row = $table->get($key)` | Restituisce una singola riga in base alla chiave primaria | +| `$row = $table->fetch()` | Restituisce la riga corrente e fa avanzare il puntatore a quella successiva | +| `$array = $table->fetchPairs()` | Crea un array associativo dai risultati | +| `$array = $table->fetchAll()` | Restituisce tutte le righe come array | +| `count($table)` | Restituisce il numero di righe nell'oggetto Selection | + +L'oggetto [ActiveRow |api:Nette\Database\Table\ActiveRow] è di sola lettura. Ciò significa che non è possibile modificare i valori delle sue proprietà. Questa restrizione garantisce la coerenza dei dati e previene effetti collaterali imprevisti. I dati vengono prelevati dal database e qualsiasi modifica deve essere effettuata in modo esplicito e controllato. + + +`foreach` - Iterazione di tutte le righe +---------------------------------------- -Esempi di utilizzo: +Il modo più semplice per eseguire una query e recuperare le righe è l'iterazione con il ciclo `foreach`. Il ciclo esegue automaticamente la query SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $chiave = chiave primaria, $libro = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtrare in base al valore di un'altra tabella .[#toc-joining-key] ------------------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Esegue una query SQL e restituisce una riga in base alla sua chiave primaria o `null` se non esiste. + +```php +$book = $explorer->table('book')->get(123); // restituisce ActiveRow con ID 123 o null +if ($book) { + echo $book->title; +} +``` -Molto spesso è necessario filtrare i risultati in base a una condizione che coinvolge un'altra tabella del database. Questi tipi di condizioni richiedono join di tabelle. Tuttavia, non è più necessario scriverle. -Supponiamo di dover ottenere tutti i libri il cui autore si chiama 'Jon'. Tutto ciò che occorre scrivere è la chiave di unione della relazione e il nome della colonna nella tabella unita. La chiave di unione deriva dalla colonna che si riferisce alla tabella che si vuole unire. Nel nostro esempio (si veda lo schema del db) si tratta della colonna `author_id`, ed è sufficiente utilizzarne solo la prima parte - `author` (il suffisso `_id` può essere omesso). `name` è una colonna della tabella `author` che vogliamo utilizzare. Una condizione per il traduttore di libri (che è collegata alla colonna `translator_id` ) può essere creata altrettanto facilmente. +fetch(): ?ActiveRow .[method] +----------------------------- + +Restituisce una riga e fa avanzare il puntatore interno a quella successiva. Se non ci sono più righe, restituisce `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); +} ``` -La logica delle chiavi di unione è guidata dall'implementazione delle [Convenzioni |api:Nette\Database\Conventions]. Si consiglia di utilizzare [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], che analizza le chiavi esterne e consente di lavorare facilmente con queste relazioni. -La relazione tra il libro e il suo autore è 1:N. È possibile anche la relazione inversa. La chiamiamo **backjoin**. Vediamo un altro esempio. Vogliamo recuperare tutti gli autori che hanno scritto più di 3 libri. Per rendere la join inversa, utilizziamo l'istruzione `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY` e anche la condizione deve essere scritta sotto forma di istruzione `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Restituisce i risultati come array associativo. Il primo argomento specifica il nome della colonna da usare come chiave dell'array e il secondo argomento specifica il nome della colonna da usare come valore: ```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', ...] +``` + +Se si specifica solo la colonna chiave, il valore sarà l'intera riga, cioè l'oggetto `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Si sarà notato che l'espressione di join si riferisce al libro, ma non è chiaro se il join avvenga attraverso `author_id` o `translator_id`. Nell'esempio precedente, Selection si unisce attraverso la colonna `author_id` perché è stata trovata una corrispondenza con la tabella di origine - la tabella `author`. Se non ci fosse tale corrispondenza e ci fossero più possibilità, Nette lancerebbe [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Se `null` è specificato come chiave, la matrice sarà indicizzata numericamente a partire da zero: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` -Per effettuare un join attraverso la colonna `translator_id`, fornire un parametro opzionale all'interno dell'espressione di join. +È anche possibile passare un callback come parametro, che restituirà il valore stesso o una coppia chiave-valore per ogni riga. Se il callback restituisce solo un valore, la chiave sarà la chiave primaria della riga: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Primo libro (Jan Novak)', ...] + +// Il callback può anche restituire un array con una coppia di chiavi e valori: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Primo libro' => 'Jan Novak', ...] ``` -Vediamo alcune espressioni di unione più difficili. -Vogliamo trovare tutti gli autori che hanno scritto qualcosa su PHP. Tutti i libri hanno un tag, quindi dovremmo selezionare gli autori che hanno scritto un libro con il tag PHP. +fetchAll(): array .[method] +--------------------------- + +Restituisce tutte le righe come array associativo di oggetti `ActiveRow`, dove le chiavi sono i valori della chiave primaria. ```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, ...), ...] ``` -Query aggregate .[#toc-aggregate-queries] ------------------------------------------ +count(): int .[method] +---------------------- -| `$table->count('*')` | Ottenere il numero di righe -| `$table->count("DISTINCT $column")` | Ottieni il numero di valori distinti -| `$table->min($column)` | Ottieni il valore minimo -| `$table->max($column)` | Ottieni il valore massimo -| `$table->sum($column)` | Ottenere la somma di tutti i valori -| `$table->aggregation("GROUP_CONCAT($column)")` | Eseguire qualsiasi funzione di aggregazione +Il metodo `count()` senza parametri restituisce il numero di righe nell'oggetto `Selection`: -.[caution] -Il metodo `count()` senza parametri specificati seleziona tutti i record e restituisce la dimensione dell'array, il che è molto inefficiente. Ad esempio, se è necessario calcolare il numero di righe per la paginazione, specificare sempre il primo argomento. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativo +``` + +Nota: `count()` con un parametro esegue la funzione di aggregazione COUNT nel database, come descritto di seguito. + + +ActiveRow::toArray(): array .[method] +------------------------------------- +Converte l'oggetto `ActiveRow` in un array associativo in cui le chiavi sono nomi di colonne e i valori sono i dati corrispondenti. -Escaping e citazione .[#toc-escaping-quoting] -============================================= +```php +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray sarà ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Aggregazione .[#toc-aggregation] +================================ + +La classe `Selection` fornisce metodi per eseguire facilmente funzioni di aggregazione (COUNT, SUM, MIN, MAX, AVG, ecc.). + +.[language-php] +| `count($expr)` | Conta il numero di righe | +| `min($expr)` | Restituisce il valore minimo di una colonna | +| `max($expr)` | Restituisce il valore massimo di una colonna | +| `sum($expr)` | Restituisce la somma dei valori di una colonna | +| `aggregation($function)` | Consente qualsiasi funzione di aggregazione, come ad esempio `AVG()` o `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Esegue una query SQL con la funzione COUNT e restituisce il risultato. Questo metodo viene utilizzato per determinare quante righe corrispondono a una determinata condizione: + +```php +$count = $table->count('*'); // SELEZIONARE COUNT(*) DA `tabella' +$count = $table->count('DISTINCT column'); // SELEZIONARE COUNT(DISTINCT `colonna') DA `tabella` +``` + +Nota: [count() |#count()] senza un parametro restituisce semplicemente il numero di righe nell'oggetto `Selection`. -Database Explorer è intelligente e consente di sfuggire ai parametri e agli identificatori di virgolette. Tuttavia, è necessario seguire queste regole di base: -- le parole chiave, le funzioni e le procedure devono essere maiuscole -- le colonne e le tabelle devono essere minuscole -- Passare le variabili come parametri, non concatenarle. +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +I metodi `min()` e `max()` restituiscono i valori minimi e massimi della colonna o dell'espressione specificata: ```php -->where('name like ?', 'John'); // WRONG! generates: `name` `like` ? -->where('name LIKE ?', 'John'); // CORRECT +// SELEZIONA MAX(`prezzo`) DA `prodotti` DOVE `attivo` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` -->where('KEY = ?', $value); // WRONG! KEY is a keyword -->where('key = ?', $value); // CORRECT. generates: `key` = ? -->where('name = ' . $name); // WRONG! sql injection! -->where('name = ?', $name); // CORRECT +sum(string $expr): int .[method] +-------------------------------- -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // WRONG! pass variables as parameters, do not concatenate -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORRECT +Restituisce la somma dei valori della colonna o dell'espressione specificata: + +```php +// SELEZIONA SOMMA(`prezzo` * `voci_in_magazzino`) DA `prodotti` DOVE `attivo` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -Un uso errato può produrre falle nella sicurezza +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -Recuperare i dati .[#toc-fetching-data] -======================================= +Consente l'esecuzione di qualsiasi funzione di aggregazione. -| `foreach ($table as $id => $row)` | Iterare su tutte le righe del risultato -| `$row = $table->get($id)` | Ottenere una singola riga con ID $id dalla tabella -| `$row = $table->fetch()` | Ottenere la riga successiva dal risultato -| `$array = $table->fetchPairs($key, $value)` | Recuperare tutti i valori in un array associativo -| `$array = $table->fetchPairs($value)` | Recupera tutte le righe nell'array associativo -| `count($table)` | Ottenere il numero di righe nell'insieme dei risultati +```php +// Calcola il prezzo medio dei prodotti di una categoria +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Combina i tag dei prodotti in un'unica stringa +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` +Se è necessario aggregare risultati che a loro volta derivano da un'aggregazione e da un raggruppamento (ad esempio, `SUM(value)` su righe raggruppate), si specifica la funzione di aggregazione da applicare a questi risultati intermedi come secondo argomento: + +```php +// Calcola il prezzo totale dei prodotti in magazzino per ogni categoria, quindi somma questi prezzi +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +In questo esempio, calcoliamo innanzitutto il prezzo totale dei prodotti di ogni categoria (`SUM(price * stock) AS category_total`) e raggruppiamo i risultati per `category_id`. Quindi utilizziamo `aggregation('SUM(category_total)', 'SUM')` per sommare questi totali parziali. Il secondo argomento `'SUM'` specifica la funzione di aggregazione da applicare ai risultati intermedi. + + +Inserimento, aggiornamento e cancellazione .[#toc-insert-update-delete] +======================================================================= + +Nette Database Explorer semplifica l'inserimento, l'aggiornamento e la cancellazione dei dati. Tutti i metodi menzionati lanciano un `Nette\Database\DriverException` in caso di errore. -Inserire, aggiornare e cancellare .[#toc-insert-update-delete] -============================================================== -Il metodo `insert()` accetta una serie di oggetti Traversable (ad esempio [ArrayHash |utils:arrays#ArrayHash] che restituisce i [moduli |forms:]): +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Inserisce nuovi record in una tabella. + +**Inserimento di un singolo record:** + +Il nuovo record viene passato come array associativo o oggetto iterabile (come `ArrayHash` usato nei [form |forms:]), dove le chiavi corrispondono ai nomi delle colonne della tabella. + +Se la tabella ha una chiave primaria definita, il metodo restituisce un oggetto `ActiveRow`, che viene ricaricato dal database per riflettere eventuali modifiche apportate a livello di database (ad esempio, trigger, valori predefiniti delle colonne o calcoli di autoincremento). Ciò garantisce la coerenza dei dati e l'oggetto contiene sempre i dati correnti del database. Se non è stata definita esplicitamente una chiave primaria, il metodo restituisce i dati di input come array. ```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 è un'istanza di ActiveRow contenente i dati completi della riga inserita, +// compreso l'ID autogenerato e le modifiche apportate dai trigger. +echo $row->id; // Fornisce l'ID dell'utente appena inserito +echo $row->created_at; // Fornisce l'ora di creazione, se impostata da un trigger. ``` -Se la chiave primaria è definita sulla tabella, viene restituito un oggetto ActiveRow contenente la riga inserita. +**Inserimento di più record contemporaneamente:** -Inserimento multiplo: +Il metodo `insert()` consente di inserire più record con una singola query SQL. In questo caso, restituisce il numero di righe inserite. ```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 sarà 2 +``` + +È anche possibile passare come parametro un oggetto `Selection` con una selezione di dati. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -I file o gli oggetti DateTime possono essere passati come parametri: +**Inserimento di valori speciali:** + +I valori possono includere file, oggetti `DateTime` o letterali SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // converte nel formato del database + 'avatar' => fopen('image.jpg', 'rb'), // inserisce il contenuto del file binario + 'uuid' => $explorer::literal('UUID()'), // chiama la funzione UUID() ]); ``` -Aggiornamento (restituisce il conteggio delle righe interessate): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Aggiorna le righe di una tabella in base a un filtro specificato. Restituisce il numero di righe effettivamente modificate. + +Le colonne da aggiornare sono passate come array associativo o oggetto iterabile (come `ArrayHash` usato nei [moduli |forms:]), dove le chiavi corrispondono ai nomi delle colonne della tabella: ```php -$count = $explorer->table('users') - ->where('id', 10) // must be called before 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) +// AGGIORNARE `utenti` SET `nome` = 'John Smith', `anno` = 1994 DOVE `id` = 10 ``` -Per l'aggiornamento si possono utilizzare gli operatori `+=` e `-=`: +Per modificare i valori numerici, si possono usare gli operatori `+=` e `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // aumenta il valore della colonna "punti" di 1 + 'coins-=' => 1, // diminuisce il valore della colonna "monete" di 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `utenti` SET `punti` = `punti` + 1, `monete` = `monete` - 1 WHERE `id` = 10 ``` -Eliminazione (restituisce il conteggio delle righe eliminate): + +Selection::delete(): int .[method] +---------------------------------- + +Elimina le righe da una tabella in base a un filtro specificato. Restituisce il numero di righe eliminate. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// CANCELLARE DA `utenti` DOVE `id` = 10 ``` +.[caution] +Quando si chiama `update()` o `delete()`, assicurarsi di usare `where()` per specificare le righe da aggiornare o eliminare. Se `where()` non viene utilizzato, l'operazione verrà eseguita sull'intera tabella! + -Lavorare con le relazioni .[#toc-working-with-relationships] -============================================================ +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Aggiorna i dati di una riga del database rappresentata dall'oggetto `ActiveRow`. Accetta come parametro dati iterabili, dove le chiavi sono nomi di colonne. Per modificare i valori numerici, si possono usare gli operatori `+=` e `-=`: -Ha una relazione .[#toc-has-one-relation] ------------------------------------------ -La relazione Has one è un caso d'uso comune. Il libro *ha un* autore. Il libro *ha un* traduttore. L'ottenimento di una riga correlata avviene principalmente con il metodo `ref()`. Accetta due argomenti: il nome della tabella di destinazione e la colonna di unione di origine. Vedere l'esempio: +Dopo l'aggiornamento, `ActiveRow` viene ricaricato automaticamente dal database per riflettere le modifiche apportate a livello di database (ad esempio, i trigger). Il metodo restituisce `true` solo se si è verificata una modifica reale dei dati. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // incrementa il conteggio delle visualizzazioni +]); +echo $article->views; // Emette il conteggio delle visualizzazioni correnti ``` -Nell'esempio precedente si recupera la voce relativa all'autore dalla tabella `author`; la chiave primaria dell'autore viene cercata dalla colonna `book.author_id`. Il metodo Ref() restituisce un'istanza di ActiveRow o null se non esiste una voce appropriata. La riga restituita è un'istanza di ActiveRow, quindi si può lavorare con essa come con la voce del libro. +Questo metodo aggiorna solo una riga specifica del database. Per gli aggiornamenti in blocco di più righe, utilizzare il metodo [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Elimina una riga dal database rappresentata dall'oggetto `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Elimina il libro con ID 1 +``` + +Questo metodo elimina solo una riga specifica nel database. Per l'eliminazione in blocco di più righe, utilizzare il metodo [Selection::delete() |#Selection::delete()]. -// o direttamente -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +Relazioni tra tabelle .[#toc-relationships-between-tables] +========================================================== + +Nei database relazionali, i dati sono suddivisi in più tabelle e collegati tramite chiavi esterne. Nette Database Explorer offre un modo rivoluzionario di lavorare con queste relazioni, senza scrivere query JOIN o richiedere alcuna configurazione o generazione di entità. + +Per la dimostrazione, utilizzeremo il **database di esempio**[(disponibile su GitHub |https://github.com/nette-examples/books]). Il database comprende le seguenti tabelle: + +- `author` - autori e traduttori (colonne `id`, `name`, `web`, `born`) +- `book` - libri (colonne `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - tag (colonne `id`, `name`) +- `book_tag` - tabella di collegamento tra libri e tag (colonne `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Struttura del database .<> + +In questo esempio di database di libri, troviamo diversi tipi di relazioni (semplificate rispetto alla realtà): + +- **Uno-a-molti (1:N)** - Ogni libro **ha un** autore; un autore può scrivere **molti** libri. +- **Zero-a-molti (0:N)** - Un libro **può avere** un traduttore; un traduttore può tradurre **molti** libri. +- **Zero-a-uno (0:1)** - Un libro **può avere** un seguito. +- **Molti a molti (M:N)** - Un libro **può avere diversi** tag, e un tag può essere assegnato a **molti** libri. + +In queste relazioni, c'è sempre una **tabella padre** e una **tabella figlio**. Ad esempio, nella relazione tra autori e libri, la tabella `author` è il genitore, mentre la tabella `book` è il figlio: si può pensare che un libro "appartenga" sempre a un autore. Questo si riflette anche nella struttura del database: la tabella figlio `book` contiene la chiave esterna `author_id`, che fa riferimento alla tabella padre `author`. + +Se vogliamo visualizzare i libri insieme ai nomi degli autori, abbiamo due possibilità. O recuperiamo i dati con un'unica query SQL con un JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Oppure recuperiamo i dati in due fasi - prima i libri, poi i loro autori - e li assembliamo in PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Il libro ha anche un traduttore, quindi ottenere il nome del traduttore è abbastanza facile. +Il secondo approccio è, sorprendentemente, **più efficiente**. I dati vengono recuperati una sola volta e possono essere utilizzati meglio nella cache. Questo è esattamente il modo in cui funziona Nette Database Explorer: gestisce tutto sotto il cofano e fornisce un'API pulita: + ```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 è un record della tabella "author" (autore) + echo 'translated by: ' . $book->translator?->name; +} ``` -Tutto questo va bene, ma è un po' macchinoso, non credete? Database Explorer contiene già le definizioni delle chiavi esterne, quindi perché non usarle automaticamente? Facciamolo! -Se chiamiamo una proprietà che non esiste, ActiveRow cerca di risolvere il nome della proprietà chiamante come una relazione 'ha una'. Ottenere questa proprietà equivale a chiamare il metodo ref() con un solo argomento. Chiameremo l'unico argomento **chiave**. La chiave sarà risolta in una particolare relazione di chiave esterna. La chiave passata viene confrontata con le colonne della riga e, se corrisponde, la chiave esterna definita sulla colonna corrispondente viene utilizzata per ottenere i dati dalla tabella di destinazione. Vedere l'esempio: +Accesso alla tabella dei genitori .[#toc-accessing-the-parent-table] +-------------------------------------------------------------------- + +L'accesso alla tabella dei genitori è semplice. Si tratta di relazioni come *un libro ha un autore* o *un libro può avere un traduttore*. È possibile accedere al record correlato tramite la proprietà dell'oggetto `ActiveRow` - il nome della proprietà corrisponde al nome della colonna della chiave esterna senza il suffisso `id`: ```php -$book->author->name; -// come -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // trova l'autore tramite la colonna "author_id". +echo $book->translator?->name; // trova il traduttore tramite la colonna "translator_id". ``` -L'istanza ActiveRow non ha una colonna autore. Tutte le colonne dei libri vengono cercate per trovare una corrispondenza con *chiave*. In questo caso, la corrispondenza significa che il nome della colonna deve contenere la chiave. Quindi, nell'esempio precedente, la colonna `author_id` contiene la stringa 'autore' ed è quindi abbinata alla chiave 'autore'. Se si desidera ottenere il traduttore del libro, è sufficiente utilizzare, ad esempio, 'traduttore' come chiave, perché la chiave 'traduttore' corrisponderà alla colonna `translator_id`. Per ulteriori informazioni sulla logica di corrispondenza delle chiavi, consultare il capitolo [Espressioni di unione |#joining-key]. +Quando si accede alla proprietà `$book->author`, Explorer cerca una colonna nella tabella `book` che contenga la stringa `author` (ad esempio, `author_id`). In base al valore di questa colonna, recupera il record corrispondente dalla tabella `author` e lo restituisce come oggetto `ActiveRow`. Allo stesso modo, `$book->translator` utilizza la colonna `translator_id`. Poiché la colonna `translator_id` può contenere `null`, viene utilizzato l'operatore `?->`. + +Un approccio alternativo è fornito dal metodo `ref()`, che accetta due argomenti - il nome della tabella di destinazione e la colonna di collegamento - e restituisce un'istanza `ActiveRow` o `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; // link all'autore +echo $book->ref('author', 'translator_id')->name; // link al traduttore ``` -Se si desidera recuperare più libri, si deve utilizzare lo stesso approccio. Nette Database Explorer recupererà autori e traduttori per tutti i libri recuperati in una sola volta. +Il metodo `ref()` è utile se non è possibile utilizzare l'accesso basato sulle proprietà, ad esempio quando la tabella contiene una colonna con lo stesso nome della proprietà (`author`). In altri casi, l'uso dell'accesso basato sulle proprietà è consigliato per una migliore leggibilità. + +Explorer ottimizza automaticamente le query al database. Quando si iterano i libri e si accede ai loro record correlati (autori, traduttori), Explorer non genera una query per ogni singolo libro. Esegue invece solo **una query SELECT per ogni tipo di relazione**, riducendo in modo significativo il carico del database. Ad esempio: ```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; } ``` -Il codice eseguirà solo queste 3 query: +Questo codice eseguirà solo tre query ottimizzate al database: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +La logica per identificare la colonna di collegamento è definita dall'implementazione di [Conventions |api:Nette\Database\Conventions]. Si consiglia di utilizzare [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], che analizza le chiavi esterne e consente di lavorare senza problemi con le relazioni esistenti tra le tabelle. -Ha molte relazioni .[#toc-has-many-relation] --------------------------------------------- -La relazione "ha molti" è solo un'inversione della relazione "ha uno". L'autore *ha* scritto *molti* libri. L'autore *ha* tradotto *molti* libri. Come si può vedere, questo tipo di relazione è un po' più difficile perché la relazione è 'nominativa' ('scritto', 'tradotto'). L'istanza ActiveRow ha il metodo `related()`, che restituisce un array di voci correlate. Anche le voci sono istanze di ActiveRow. Vedere l'esempio qui sotto: +Accesso alla tabella figlio .[#toc-accessing-the-child-table] +------------------------------------------------------------- + +L'accesso alla tabella dei figli funziona in senso inverso. Ora chiediamo *quali libri ha scritto questo autore* o *quali libri ha tradotto questo traduttore*. Per questo tipo di query, si usa il metodo `related()`, che restituisce un oggetto `Selection` con i record correlati. Ecco un esempio: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Visualizza tutti i libri scritti dall'autore foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Esce tutti i libri tradotti dall'autore foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Metodo `related()` Il metodo accetta la descrizione completa del join passata come due argomenti o come un argomento unito da un punto. Il primo argomento è la tabella di destinazione, il secondo è la colonna di destinazione. +Il metodo `related()` accetta la descrizione della relazione come un singolo argomento usando la notazione a punti o come due argomenti separati: ```php -$author->related('book.translator_id'); -// come -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // singolo argomento +$author->related('book', 'translator_id'); // due argomenti ``` -È possibile utilizzare l'euristica di Nette Database Explorer basata sulle chiavi esterne e fornire solo l'argomento **chiave**. La chiave verrà confrontata con tutte le chiavi esterne che puntano alla tabella corrente (tabella`author` ). Se c'è una corrispondenza, Nette Database Explorer utilizzerà questa chiave esterna, altrimenti lancerà [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] o [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Per ulteriori informazioni sulla logica di corrispondenza delle chiavi, consultare il capitolo [Espressioni di unione |#joining-key]. +Explorer può rilevare automaticamente la colonna di collegamento corretta in base al nome della tabella padre. In questo caso, il collegamento avviene tramite la colonna `book.author_id` perché il nome della tabella di origine è `author`: -Naturalmente è possibile chiamare i metodi correlati per tutti gli autori recuperati; Nette Database Explorer recupererà nuovamente i libri appropriati in una sola volta. +```php +$author->related('book'); // utilizza book.author_id +``` + +Se esistono più collegamenti possibili, Explorer lancia un'eccezione [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Naturalmente, è possibile utilizzare il metodo `related()` anche quando si iterano più record in un ciclo, e anche in questo caso Explorer ottimizzerà automaticamente le query: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -L'esempio precedente eseguirà solo due query: +Questo codice genera solo due query SQL efficienti: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors +``` + + +Relazione molti-a-molti .[#toc-many-to-many-relationship] +--------------------------------------------------------- + +Per una relazione molti-a-molti (M:N), è necessaria una **tabella di giunzione** (nel nostro caso, `book_tag`). Questa tabella contiene due colonne di chiave esterna (`book_id`, `tag_id`). Ogni colonna fa riferimento alla chiave primaria di una delle tabelle collegate. Per recuperare i dati correlati, si recuperano prima i record dalla tabella di collegamento usando `related('book_tag')`, e poi si prosegue con i dati di destinazione: + +```php +$book = $explorer->table('book')->get(1); +// Visualizza i nomi dei tag assegnati al libro +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // recupera il nome del tag attraverso la tabella dei collegamenti +} + +$tag = $explorer->table('tag')->get(1); +// Direzione opposta: visualizza i titoli dei libri con questo tag +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // recupera il titolo del libro +} +``` + +Explorer ottimizza nuovamente le query SQL in modo efficiente: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Interrogazione attraverso tabelle correlate .[#toc-querying-through-related-tables] +----------------------------------------------------------------------------------- + +Nei metodi `where()`, `select()`, `order()`, e `group()`, si possono usare notazioni speciali per accedere alle colonne di altre tabelle. Explorer crea automaticamente le JOIN necessarie. + +La **notazione a punti** (`parent_table.column`) è usata per le relazioni 1:N viste dalla prospettiva della tabella madre: + +```php +$books = $explorer->table('book'); + +// Trova i libri il cui nome dell'autore inizia con "Jon". +$books->where('author.name LIKE ?', 'Jon%'); + +// Ordina i libri per nome dell'autore in ordine decrescente +$books->order('author.name DESC'); + +// Emette il titolo del libro e il nome dell'autore +$books->select('book.title, author.name'); ``` +La **notezione a colonne** è usata per le relazioni 1:N dal punto di vista della tabella madre: + +```php +$authors = $explorer->table('author'); + +// Trova gli autori che hanno scritto un libro con 'PHP' nel titolo +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Conta il numero di libri per ogni autore +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Nell'esempio precedente con la notazione a due punti (`:book.title`), la colonna della chiave esterna non è specificata esplicitamente. Explorer rileva automaticamente la colonna corretta in base al nome della tabella padre. In questo caso, si unisce attraverso la colonna `book.author_id` perché il nome della tabella di origine è `author`. Se esistono più connessioni possibili, Explorer lancia l'eccezione [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +La colonna di collegamento può essere specificata esplicitamente tra parentesi: + +```php +// Trova gli autori che hanno tradotto un libro con il termine "PHP" nel titolo. +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Le notazioni possono essere concatenate per accedere ai dati di più tabelle: + +```php +// Trova gli autori di libri etichettati con "PHP". +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` -Creazione manuale di Explorer .[#toc-creating-explorer-manually] -================================================================ -È possibile creare una connessione al database utilizzando la configurazione dell'applicazione. In questi casi viene creato un servizio `Nette\Database\Explorer`, che può essere passato come dipendenza tramite il contenitore DI. +Estensione delle condizioni di JOIN .[#toc-extending-conditions-for-join] +------------------------------------------------------------------------- -Tuttavia, se Nette Database Explorer viene utilizzato come strumento autonomo, è necessario creare manualmente un'istanza dell'oggetto `Nette\Database\Explorer`. +Il metodo `joinWhere()` aggiunge condizioni aggiuntive alle unioni di tabelle in SQL dopo la parola chiave `ON`. + +Ad esempio, supponiamo di voler trovare i libri tradotti da un traduttore specifico: + +```php +// Trova i libri tradotti da un traduttore chiamato 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN autore traduttore ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +Nella condizione `joinWhere()` è possibile utilizzare gli stessi costrutti del metodo `where()`: operatori, segnaposto, array di valori o espressioni SQL. + +Per query più complesse con più JOIN, è possibile definire degli alias di tabella: ```php -// $storage implementa Nette\Caching\Storage: -$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); +$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) +``` + +Si noti che mentre il metodo `where()` aggiunge condizioni alla clausola `WHERE`, il metodo `joinWhere()` estende le condizioni della clausola `ON` durante i join di tabelle. + + +Creare manualmente Explorer .[#toc-manually-creating-explorer] +============================================================== + +Se non si usa il contenitore Nette DI, si può creare manualmente un'istanza di `Nette\Database\Explorer`: + +```php +use Nette\Database; + +// $storage implementa Nette\Caching\Storage, ad es: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// connessione al database +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// gestisce la riflessione della struttura del database +$structure = new Database\Structure($connection, $storage); +// definisce le regole per la mappatura dei nomi delle tabelle, delle colonne e delle chiavi esterne +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/it/security.texy b/database/it/security.texy new file mode 100644 index 0000000000..4e42bff470 --- /dev/null +++ b/database/it/security.texy @@ -0,0 +1,145 @@ +Rischi per la sicurezza +*********************** + +.[perex] +I database contengono spesso dati sensibili e consentono operazioni pericolose. Nette Database offre una serie di funzioni di sicurezza. Tuttavia, è fondamentale capire la differenza tra API sicure e non sicure. + + +Iniezione SQL .[#toc-sql-injection] +=================================== + +L'iniezione SQL è il rischio di sicurezza più grave quando si lavora con i database. Si verifica quando un input dell'utente non controllato diventa parte di una query SQL. Un aggressore può iniettare i propri comandi SQL, ottenendo o modificando i dati nel database. + +```php +// Codice non sicuro - vulnerabile all'iniezione SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// L'attaccante può inserire qualcosa come: ' OR '1'='1 +// La query risultante sarà: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Questo restituisce tutti gli utenti! +``` + +Lo stesso vale per Database Explorer: + +```php +// ❌ CODICE NON SICURO +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Query parametriche sicure .[#toc-safe-parameterized-queries] +============================================================ + +Il modo sicuro per inserire valori nelle query SQL è quello delle query parametrizzate. Nette Database offre diversi modi per utilizzarle. + + +Punti interrogativi segnaposto .[#toc-placeholder-question-marks] +----------------------------------------------------------------- + +Il metodo più semplice è quello di utilizzare dei punti interrogativi segnaposto: + +```php +// Query parametriche sicure +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Condizione sicura in Explorer +$table->where('name = ?', $_GET['name']); +``` + +Lo stesso vale per tutti gli altri metodi di Database Explorer che consentono di inserire espressioni con punti interrogativi e parametri segnaposto. + +.[warning] +I valori devono essere di tipo scalare (`string`, `int`, `float`, `bool`) o `null`. Se, ad esempio, è un array, Nette Database includerà tutti i suoi elementi nella query SQL, il che può essere indesiderato, `$_GET['name']` è un array, Nette Database includerà tutti i suoi elementi nella query SQL, il che può essere indesiderato. + + +Array di valori .[#toc-value-arrays] +------------------------------------ + +Per le clausole `INSERT`, `UPDATE` o `WHERE` si possono utilizzare array di valori: + +```php +// INSERIMENTO sicuro +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// AGGIORNAMENTO sicuro +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database esegue automaticamente l'escape di tutti i valori passati attraverso le query parametrizzate. Tuttavia, è necessario garantire il corretto tipo di dati dei parametri. + + +Le chiavi di array non sono un'API sicura .[#toc-array-keys-are-not-a-safe-api] +=============================================================================== + +Mentre i valori negli array sono sicuri, lo stesso non si può dire per le chiavi: + +```php +// Codice non sicuro: le chiavi possono contenere un'iniezione SQL. +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Per i comandi `INSERT` e `UPDATE` si tratta di una falla di sicurezza critica: un utente malintenzionato potrebbe inserire o modificare qualsiasi colonna del database. Ad esempio, potrebbe impostare `is_admin = 1` o inserire dati arbitrari in colonne sensibili. + +Nelle condizioni di `WHERE`, questo è ancora più pericoloso perché permette la **Enumerazione SQL**, una tecnica per recuperare gradualmente informazioni sul database. Un utente malintenzionato potrebbe tentare di esplorare gli stipendi dei dipendenti iniettando in `$_GET` questo tipo di dati: + +```php +$_GET = ['salary >', 100000]; // inizia a determinare le fasce salariali +``` + +Il problema principale, tuttavia, è che le condizioni di `WHERE` supportano espressioni SQL nelle chiavi: + +```php +// Uso legittimo degli operatori nelle chiavi +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// Non sicuro: l'attaccante può iniettare il proprio SQL. +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // permette all'attaccante di ottenere stipendi da amministratore +``` + +Si tratta ancora una volta di **Iniezione SQL**. + + +Eliminazione delle colonne .[#toc-whitelisting-columns] +------------------------------------------------------- + +Se si desidera consentire agli utenti di scegliere le colonne, utilizzare sempre una whitelist: + +```php +// ✅ Elaborazione sicura - solo colonne consentite +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Identificatori dinamici .[#toc-dynamic-identifiers] +=================================================== + +Per i nomi dinamici di tabelle e colonne, utilizzare il segnaposto `?name`: + +```php +// ✅ Uso sicuro di identificatori affidabili +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - non utilizzare mai l'input dell'utente +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Il simbolo `?name` deve essere utilizzato solo per i valori affidabili definiti nel codice dell'applicazione. Per i valori forniti dall'utente, utilizzare nuovamente una whitelist. diff --git a/database/pl/@left-menu.texy b/database/pl/@left-menu.texy index fdd85b8867..6abacd0802 100644 --- a/database/pl/@left-menu.texy +++ b/database/pl/@left-menu.texy @@ -4,3 +4,4 @@ Databáze - [Odkrywca |Explorer] - [Refleksja |Reflection] - [Konfiguracja |configuration] +- [Zagrożenia bezpieczeństwa |security] diff --git a/database/pl/explorer.texy b/database/pl/explorer.texy index e3295102d7..533db15e3b 100644 --- a/database/pl/explorer.texy +++ b/database/pl/explorer.texy @@ -3,548 +3,927 @@ Eksplorator baz danych
-Nette Database Explorer (dawniej Nette Database Table, NDBT) znacznie ułatwia pobieranie danych z bazy danych bez konieczności pisania zapytań SQL. +Nette Database Explorer to potężna warstwa, która znacznie upraszcza pobieranie danych z bazy danych bez konieczności pisania zapytań SQL. -- Zadaje skuteczne pytania -- nie przekazuje zbędnych danych -- ma elegancką składnię +- Praca z danymi jest naturalna i łatwa do zrozumienia +- Generuje zoptymalizowane zapytania SQL, które pobierają tylko niezbędne dane +- Zapewnia łatwy dostęp do powiązanych danych bez konieczności pisania zapytań JOIN +- Działa natychmiast bez konieczności konfiguracji lub generowania encji
-Korzystanie z Database Explorera rozpoczyna się od tabeli poprzez wywołanie metody `table()` nad obiektem [api:Nette\Database\Explorer]. Najłatwiejszy sposób jej uzyskania [opisany |core#Connection-and-Configuration] jest [tutaj |core#Connection-and-Configuration], ale jeśli korzystasz z Nette Database Explorera samodzielnie, możesz również [utworzyć ją ręcznie |#Ruční vytvoření Explorer]. +Nette Database Explorer jest rozszerzeniem niskopoziomowej warstwy [Nette Database Core |core], która dodaje wygodne podejście obiektowe do zarządzania bazą danych. + +Praca z Explorerem rozpoczyna się od wywołania metody `table()` na obiekcie [api:Nette\Database\Explorer] (jak ją uzyskać opisano [tutaj |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // nazwa tabeli to 'book' +$books = $explorer->table('book'); // "book" to nazwa tabeli ``` -Zwraca obiekt [Selection |api:Nette\Database\Table\Selection], nad którym możemy iterować, aby przejść przez wszystkie książki. Wiersze są instancjami [ActiveRow |api:Nette\Database\Table\ActiveRow] i możemy z nich bezpośrednio odczytywać dane. +Metoda ta zwraca obiekt [Selection |api:Nette\Database\Table\Selection], który reprezentuje zapytanie SQL. Do tego obiektu można podłączyć dodatkowe metody filtrowania i sortowania wyników. Zapytanie jest składane i wykonywane tylko wtedy, gdy dane są wymagane, na przykład przez iterację z `foreach`. Każdy wiersz jest reprezentowany przez obiekt [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // wyprowadza kolumnę "title + echo $book->author_id; // wyprowadza kolumnę "author_id } ``` -Wybór jednego konkretnego wiersza odbywa się za pomocą metody `get()`, która zwraca bezpośrednio instancję ActiveRow. +Explorer znacznie upraszcza pracę z [relacjami tabel |#Vazby mezi tabulkami]. Poniższy przykład pokazuje, jak łatwo możemy wyprowadzić dane z powiązanych tabel (książki i ich autorzy). Zauważ, że nie trzeba pisać żadnych zapytań JOIN; Nette generuje je dla nas: ```php -$book = $explorer->table('book')->get(2); // zwróć książkę o id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // tworzy JOIN do tabeli "author +} ``` -Spróbujmy na prostym przykładzie. Musimy wybrać z bazy danych książki i ich autorów. Jest to prosty przykład wiązania 1:N. Często spotykanym rozwiązaniem jest wybieranie danych jednym zapytaniem SQL, łącząc tabele za pomocą JOIN. Inną opcją jest wybranie danych osobno, za pomocą jednego zapytania o książkę, a następnie dla każdej książki wybranie jej autora (np. za pomocą pętli foreach). Można to zoptymalizować do dwóch zapytań do bazy, jednego dla książek i jednego dla autorów - i tak właśnie robi to Nette Database Explorer. +Nette Database Explorer optymalizuje zapytania pod kątem maksymalnej wydajności. Powyższy przykład wykonuje tylko dwa zapytania SELECT, niezależnie od tego, czy przetwarzamy 10 czy 10 000 książek. -W poniższych przykładach będziemy pracować ze schematem bazy danych przedstawionym na rysunku. Istnieją wiązania OneHasMany (1:N) (autor książki, `author_id`, oraz dowolny tłumacz, `translator_id`, który może mieć wartość `null`) oraz ManyHasMany (M:N) pomiędzy książką a jej tagami. +Dodatkowo, Explorer śledzi, które kolumny są używane w kodzie i pobiera tylko te z bazy danych, oszczędzając dalszą wydajność. Zachowanie to jest w pełni automatyczne i adaptacyjne. Jeśli później zmodyfikujesz kod, aby użyć dodatkowych kolumn, Explorer automatycznie dostosuje zapytania. Nie musisz niczego konfigurować ani zastanawiać się, które kolumny będą potrzebne - pozostaw to Nette. -[Możesz znaleźć przykład, w tym schemat, na GitHubie |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Struktura bazy danych dla powyższych przykładów .<> +Filtrowanie i sortowanie .[#toc-filtering-and-sorting] +====================================================== -Poniższy kod wymienia nazwę autora każdej książki i wszystkie jej tagi. Za chwilę [omówimy |#Working-with-Relationships] dokładnie jak to działa. +Klasa `Selection` udostępnia metody filtrowania i sortowania danych. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Dodaje warunek WHERE. Wiele warunków jest łączonych za pomocą AND | +| `whereOr(array $conditions)` | Dodaje grupę warunków WHERE połączonych za pomocą OR | +| `wherePrimary($value)` | Dodaje warunek WHERE oparty na kluczu podstawowym | +| `order($columns, ...$params)` | Ustawia sortowanie za pomocą ORDER BY | +| `select($columns, ...$params)` | Określa kolumny do pobrania | +| `limit($limit, $offset = null)` | Ogranicza liczbę wierszy (LIMIT) i opcjonalnie ustawia OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Ustawia paginację | +| `group($columns, ...$params)` | Grupuje wiersze (GROUP BY) | +| `having($condition, ...$params)`| Dodaje warunek HAVING do filtrowania zgrupowanych wierszy | -foreach ($books as $book) { - echo 'tytuł: ' . $book->title; - echo 'napisane przez: ' . $book->author->name; // $book->author je řádek z tabulky 'autor' +Metody można łączyć w łańcuchy (tzw. [płynny interfejs |nette:introduction-to-object-oriented-programming#fluent-interfaces]): `$table->where(...)->order(...)->limit(...)`. - echo 'tagi: '; - foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name . ', '; // $bookTag->tag je řádek z tabulky 'tag' - } -} -``` +Metody te pozwalają również na użycie specjalnych notacji w celu uzyskania dostępu do [danych z powiązanych tabel |#Dotazování přes související tabulky]. -Będziesz mile zaskoczony tym, jak sprawnie działa warstwa bazodanowa. Powyższy przykład wykona stałą liczbę żądań, które wyglądają tak: -```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)) -``` +Ucieczka i identyfikatory .[#toc-escaping-and-identifiers] +---------------------------------------------------------- -Jeśli użyjesz pamięci [podręcznej |caching:] (jest ona domyślnie włączona), żadne niepotrzebne kolumny nie będą pobierane z bazy danych. Po pierwszym zapytaniu nazwy używanych kolumn będą przechowywane w pamięci podręcznej i tylko kolumny, których faktycznie używasz, będą pobierane z bazy danych: +Metody automatycznie unikają parametrów i cytują identyfikatory (nazwy tabel i kolumn), zapobiegając wstrzyknięciu SQL. Aby zapewnić prawidłowe działanie, należy przestrzegać kilku zasad: -```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)) +- Słowa kluczowe, nazwy funkcji, procedur itp. należy pisać **wielkimi literami**. +- Nazwy kolumn i tabel należy pisać **małymi literami**. +- Zawsze przekazuj ciągi znaków za pomocą **parametrów**. + +```php +where('name = ' . $name); // **DISASTER**: podatny na wstrzyknięcie kodu SQL +where('name LIKE "%search%"'); // **WRONG**: komplikuje automatyczne cytowanie +where('name LIKE ?', '%search%'); // **CORRECT**: wartość przekazywana jako parametr + +where('name like ?', $name); // **WRONG**: generuje: `name` `like` ? +where('name LIKE ?', $name); // **KOREKTA**: generuje: `name` LIKE ? +where('LOWER(name) = ?', $value);// **PRAWDA**: LOWER(`name`) = ? ``` -Wybór .[#toc-selections] -======================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Przyjrzyjmy się opcjom filtrowania i ograniczania selekcji przy użyciu klasy [api:Nette\Database\Table\Selection]: +Filtruje wyniki za pomocą warunków WHERE. Jego siła polega na inteligentnej obsłudze różnych typów wartości i automatycznym wybieraniu operatorów SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Ustawia WHERE używając AND jako łącznika, gdy występuje więcej niż jeden warunek -| `$table->whereOr($where)` | Ustawia WHERE używając OR jako łącznika w więcej niż jednym warunku -| `$table->order($columns)` | Ustawia ORDER BY, może być wyrażeniem `('column DESC, id DESC')` -| `$table->select($columns)` | Ustawia zwrócone kolumny, może być wyrażeniem `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Ustawia Limit i OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Ustaw paginację -| `$table->group($columns)` | Set GROUP BY -| `$table->having($having)` | Set HAVING +Podstawowe zastosowanie: -Możemy użyć tak zwanego [płynnego interfejsu |nette:introduction-to-object-oriented-programming#fluent-interfaces], na przykład `$table->where(...)->order(...)->limit(...)`. Wiele warunków `where` lub `whereOr` jest połączonych operatorem `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' +``` +Dzięki automatycznemu wykrywaniu odpowiednich operatorów nie musisz zajmować się specjalnymi przypadkami - Nette obsługuje je za Ciebie: -gdzie() .[#toc-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) +// Symbol zastępczy ? może być użyty bez operatora: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer automatycznie dodaje odpowiednie operatory w zależności od otrzymanych danych: +Metoda obsługuje również poprawnie warunki ujemne i puste tablice: -.[language-php] -| `$table->where('field', $value)` | pole = $value -| `$table->where('field', null)` | field IS NULL -| `$table->where('field > ?', $val)` | pole > $val -| `$table->where('field', [1, 2])` | pole 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 -- nie znajduje niczego +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- znajduje wszystko +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- znajduje wszystko +// $table->where('NOT id ?', $ids); // UWAGA: Ta składnia nie jest obsługiwana +``` -Placeholder (znak zapytania) działa nawet bez operatora kolumny. Kolejne połączenia są takie same: +Można również przekazać wynik innego zapytania do tabeli jako parametr, tworząc podzapytanie: ```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')); ``` -Dzięki temu możliwe jest wygenerowanie właściwego operatora na podstawie wartości: +Warunki mogą być również przekazywane jako tablica, z elementami połączonymi za 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', +]); ``` -Wybór poprawnie obsługuje warunki negatywne i może również pracować z pustymi polami: +W tablicy można użyć par klucz-wartość, a Nette ponownie automatycznie wybierze odpowiednie operatory: ```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], +]); +``` + +Możemy również mieszać wyrażenia SQL z symbolami zastępczymi i wieloma parametrami. Jest to przydatne w przypadku złożonych warunków z precyzyjnie zdefiniowanymi operatorami: -// powoduje to wyjątek, ta składnia nie jest obsługiwana -$table->where('NOT id ?', $ids); +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // dwa parametry są przekazywane jako tablica +]); ``` +Wielokrotne wywołania `where()` automatycznie łączą warunki za pomocą AND. -gdzieOr() .[#toc-whereor] -------------------------- -Przykład użycia bez parametrów: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Podobne do `where()`, ale łączy warunki za 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, ]); ``` -Parametry użytkowe. Jeśli nie określisz operatora, Nette Database Explorer automatycznie doda odpowiedni: +Można również użyć bardziej złożonych wyrażeń: ```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, ]); ``` -Możesz określić wyrażenie zawierające znaki zapytania wieloznaczne w kluczu, a następnie przekazać parametry w wartości: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Dodaje warunek dla klucza głównego tabeli: ```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]); +``` + +Jeśli tabela ma złożony klucz główny (np. `foo_id`, `bar_id`), przekazujemy go jako tablicę: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Przykłady użycia: +Określa kolejność zwracania wierszy. Można sortować według jednej lub kilku kolumn, w kolejności rosnącej lub malejącej, lub według niestandardowego wyrażenia: ```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 ``` -wybierz() .[#toc-select] ------------------------- +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Przykłady użycia: +Określa kolumny, które mają zostać zwrócone z bazy danych. Domyślnie Nette Database Explorer zwraca tylko te kolumny, które są faktycznie używane w kodzie. Użyj metody `select()`, gdy chcesz pobrać określone wyrażenia: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Aliasy zdefiniowane za pomocą `AS` są następnie dostępne jako właściwości obiektu `ActiveRow`: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // dostęp do aliasu +} +``` -limit() .[#toc-limit] ---------------------- -Przykłady zastosowania: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Ogranicza liczbę zwracanych wierszy (LIMIT) i opcjonalnie ustawia offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (zwraca pierwsze 10 wierszy) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +W przypadku paginacji bardziej odpowiednie jest użycie metody `page()`. + -strona() .[#toc-page] ---------------------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Alternatywny sposób ustawienia limitu i offsetu: +Upraszcza paginację wyników. Akceptuje numer strony (zaczynając od 1) i liczbę elementów na stronie. Opcjonalnie można przekazać odwołanie do zmiennej, w której przechowywana będzie całkowita liczba stron: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Pobierz numer ostatniej strony, przekaż go do zmiennej `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Grupuje wiersze według określonych kolumn (GROUP BY). Zazwyczaj jest używana w połączeniu z funkcjami agregującymi: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Zlicza liczbę produktów w każdej kategorii +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Przykłady użycia: +Ustawia warunek filtrowania zgrupowanych wierszy (HAVING). Może być używany w połączeniu z metodą `group()` i funkcjami agregującymi: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Znajduje kategorie z ponad 100 produktami +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Odczytywanie danych +=================== -Przykłady użycia: +Do odczytu danych z bazy danych dostępnych jest kilka przydatnych metod: + +.[language-php] +| `foreach ($table as $key => $row)` | Iteruje przez wszystkie wiersze, `$key` jest wartością klucza głównego, `$row` jest obiektem ActiveRow | +| `$row = $table->get($key)` | Zwraca pojedynczy wiersz według klucza podstawowego | +| `$row = $table->fetch()` | Zwraca bieżący wiersz i przesuwa wskaźnik do następnego | +| `$array = $table->fetchPairs()` | Tworzy tablicę asocjacyjną z wyników | +| `$array = $table->fetchAll()` | Zwraca wszystkie wiersze jako tablicę | +| `count($table)` | Zwraca liczbę wierszy w obiekcie Selection | + +Obiekt [ActiveRow |api:Nette\Database\Table\ActiveRow] jest tylko do odczytu. Oznacza to, że nie można zmieniać wartości jego właściwości. Ograniczenie to zapewnia spójność danych i zapobiega nieoczekiwanym efektom ubocznym. Dane są pobierane z bazy danych, a wszelkie zmiany powinny być wprowadzane w sposób jawny i kontrolowany. + + +`foreach` - Iteracja przez wszystkie wiersze +-------------------------------------------- + +Najprostszym sposobem na wykonanie zapytania i pobranie wierszy jest iteracja za pomocą pętli `foreach`. Powoduje ona automatyczne wykonanie zapytania SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = klucz główny, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Wybór według wartości z innej tabeli .[#toc-filtering-by-another-table-value] ------------------------------------------------------------------------------ +get($key): ?ActiveRow .[method] +------------------------------- + +Wykonuje zapytanie SQL i zwraca wiersz według klucza podstawowego lub `null` jeśli nie istnieje. + +```php +$book = $explorer->table('book')->get(123); // zwraca ActiveRow z ID 123 lub null +if ($book) { + echo $book->title; +} +``` -Często potrzebujemy filtrować wyniki za pomocą warunku, który dotyczy innej tabeli bazy danych. Tego typu warunek wymaga łączenia tabel, ale dzięki Nette Database Explorer nigdy nie musimy pisać ich ręcznie. -Załóżmy, że chcemy wybrać wszystkie książki napisane przez autora o nazwie `Jon`. Musimy jedynie wpisać nazwę klucza sesji join oraz nazwę kolumny połączonej tabeli. Klucz join pochodzi od nazwy kolumny, która odnosi się do tabeli, z którą chcemy się połączyć. W naszym przykładzie (patrz schemat bazy danych) jest to kolumna `author_id`, z której wystarczy użyć części - `author`. `name` to nazwa kolumny w tabeli `author`. Możemy również stworzyć warunek dla tłumacza książek, do którego dołączamy kolumnę `translator_id`. +fetch(): ?ActiveRow .[method] +----------------------------- + +Zwraca jeden wiersz i przesuwa wewnętrzny wskaźnik do następnego wiersza. Jeśli nie ma więcej wierszy, zwraca `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 tworzenia klucza join jest podana przez implementację [Conventions |api:Nette\Database\Conventions]. Zalecamy użycie [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], który parsuje klucze obce i pozwala łatwo pracować z relacjami między tabelami. -Relacja między książką a autorem wynosi 1:N. Możliwa jest również relacja odwrotna, nazywamy ją **backjoin**. Rozważmy następujący przykład. Chcemy wybrać wszystkich autorów, którzy napisali więcej niż trzy książki. Do stworzenia relacji odwrotnej używamy `:` (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`, również warunek musi być zapisany jako `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Zwraca wyniki jako tablicę asocjacyjną. Pierwszy argument określa nazwę kolumny, która ma być użyta jako klucz w tablicy, a drugi argument określa nazwę kolumny, która ma być użyta jako wartość: ```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", ...]. +``` + +Jeśli określono tylko kolumnę klucza, wartością będzie cały wiersz, tj. obiekt `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]. ``` -Być może zauważyłeś, że wyrażenie join odwołuje się do `book`, ale nie jest jasne, czy łączymy się przez `author_id` czy `translator_id`. W powyższym przykładzie Selection łączy się przez kolumnę `author_id`, ponieważ znaleziono dopasowanie do nazwy tabeli źródłowej - tabeli `author`. Gdyby nie było dopasowania i istniało wiele możliwości, Nette rzuciłoby [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Jeśli jako klucz podano `null`, tablica będzie indeksowana numerycznie począwszy od zera: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => "John Doe", 1 => "Jane Doe", ...]. +``` -Aby dołączyć poprzez `translator_id`, wystarczy dodać opcjonalny parametr do wyrażenia join. +Jako parametr można również przekazać wywołanie zwrotne, które zwróci samą wartość lub parę klucz-wartość dla każdego wiersza. Jeśli wywołanie zwrotne zwróci tylko wartość, klucz będzie kluczem podstawowym wiersza: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => "Pierwsza książka (Jan Novak)", ...]. + +// Wywołanie zwrotne może również zwrócić tablicę z parą klucz i wartość: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['First Book' => 'Jan Novak', ...]. ``` -Przyjrzyjmy się teraz bardziej złożonemu przykładowi łączenia tabel. -Chcemy wybrać wszystkich autorów, którzy napisali coś o PHP. Wszystkie książki mają etykiety, więc chcemy wybrać wszystkich autorów, którzy napisali książkę z etykietą "PHP". +fetchAll(): array .[method] +--------------------------- + +Zwraca wszystkie wiersze jako tablicę asocjacyjną obiektów `ActiveRow`, gdzie klucze są wartościami klucza głównego. ```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, ...), ...]. ``` -Agregacja wyników .[#toc-aggregate-queries] -------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Zwraca liczbę wierszy -| `$table->count("DISTINCT $column")` | Zwraca liczbę różnych wartości. -| `$table->min($column)` | Zwraca wartość minimalną -| `$table->max($column)` | Zwraca maksymalną wartość -| `$table->sum($column)` | Zwraca sumę wszystkich wartości -| `$table->aggregation("GROUP_CONCAT($column)")` | Dla każdej innej funkcji agregacji +Metoda `count()` bez parametrów zwraca liczbę wierszy w obiekcie `Selection`: -.[caution] -Metoda `count()` bez określonego parametru wybiera wszystkie rekordy i zwraca rozmiar tablicy, co jest bardzo nieefektywne. Na przykład, jeśli potrzebujesz policzyć liczbę wierszy dla paginacji, zawsze podaj pierwszy argument. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternatywa +``` + +Uwaga: `count()` z parametrem wykonuje funkcję agregacji COUNT w bazie danych, jak opisano poniżej. + + +ActiveRow::toArray(): array .[method] +------------------------------------- + +Konwertuje obiekt `ActiveRow` na tablicę asocjacyjną, w której kluczami są nazwy kolumn, a wartościami odpowiednie dane. +```php +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray będzie ['id' => 1, 'title' => '...', 'author_id' => ..., ...]. +``` + + +Agregacja .[#toc-aggregation] +============================= + +Klasa `Selection` udostępnia metody łatwego wykonywania funkcji agregacji (COUNT, SUM, MIN, MAX, AVG itp.). + +.[language-php] +| `count($expr)` | Zlicza liczbę wierszy | +| `min($expr)` | Zwraca minimalną wartość w kolumnie | +| `max($expr)` | Zwraca maksymalną wartość w kolumnie | +| `sum($expr)` | Zwraca sumę wartości w kolumnie | +| `aggregation($function)` | Umożliwia dowolną funkcję agregacji, taką jak `AVG()` lub `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Wykonuje zapytanie SQL z funkcją COUNT i zwraca wynik. Metoda ta służy do określenia, ile wierszy spełnia określony warunek: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Uwaga: [count() |#count()] bez parametru po prostu zwraca liczbę wierszy w obiekcie `Selection`. -Ucieczka i cytaty .[#toc-escaping-quoting] -========================================== -Database Explorer potrafi sprytnie wymykać się parametrom i identyfikatorom. Jednak dla prawidłowej funkcjonalności należy przestrzegać kilku zasad: +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- -- wielkie litery w słowach kluczowych, nazwach funkcji, nazwach procedur itp. -- pisać nazwy kolumn i tabel małymi literami -- ustawianie wartości za pomocą parametrów +Metody `min()` i `max()` zwracają minimalne i maksymalne wartości w określonej kolumnie lub wyrażeniu: ```php -->where('name like ?', 'John'); // ZŁY! generuje: `name` `like` ? -->where('name LIKE ?', 'John'); // PRAWDA +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + -->where('KEY = ?', $value); // PRAWDA! KEY jest słowem kluczowym -->where('key = ?', $value); // PRAWDA. generuje: `key` = ? +sum(string $expr): int .[method] +-------------------------------- -->where('name = ' . $name); // ZŁY! sql injection! -->where('name = ?', $name); // PRAWDA +Zwraca sumę wartości w określonej kolumnie lub wyrażeniu: -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // ZŁY! wartości są wstawiane przez parametr -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // PRAWDA +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -Niewłaściwe użycie może prowadzić do powstania luk w zabezpieczeniach aplikacji. +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Umożliwia wykonanie dowolnej funkcji agregacji. -Dane do odczytu .[#toc-fetching-data] -===================================== +```php +// Oblicza średnią cenę produktów w kategorii +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -| `foreach ($table as $id => $row)` | Iteruje przez wszystkie wiersze wyniku -| `$row = $table->get($id)` | Zwraca jeden wiersz o ID $id -| `$row = $table->fetch()` | Zwraca następny wiersz wyniku. -| `$array = $table->fetchPairs($key, $value)` | Zwraca wszystkie wyniki jako tablicę asocjacyjną -| `$array = $table->fetchPairs($value)` | Zwraca wszystkie wiersze jako tablicę asocjacyjną -| `count($table)` | Zwraca liczbę wierszy w wyniku. +// Łączy tagi produktów w jeden ciąg znaków +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Jeśli musimy zagregować wyniki, które same są wynikiem agregacji i grupowania (np. `SUM(value)` nad zgrupowanymi wierszami), określamy funkcję agregacji, która ma zostać zastosowana do tych wyników pośrednich jako drugi argument: + +```php +// Oblicza łączną cenę produktów w magazynie dla każdej kategorii, a następnie sumuje te ceny. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +W tym przykładzie najpierw obliczamy całkowitą cenę produktów w każdej kategorii (`SUM(price * stock) AS category_total`) i grupujemy wyniki według `category_id`. Następnie używamy `aggregation('SUM(category_total)', 'SUM')` do zsumowania tych sum częściowych. Drugi argument `'SUM'` określa funkcję agregacji, która ma być zastosowana do wyników pośrednich. Wstawianie, aktualizacja i usuwanie .[#toc-insert-update-delete] ================================================================ -Metoda `insert()` akceptuje tablice lub obiekty Traversable (na przykład [ArrayHash |utils:arrays#ArrayHash], z którym pracują [formularze |forms:]): +Nette Database Explorer upraszcza wstawianie, aktualizowanie i usuwanie danych. Wszystkie wymienione metody rzucają `Nette\Database\DriverException` w przypadku błędu. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Wstawia nowe rekordy do tabeli. + +**Wstawianie pojedynczego rekordu:**. + +Nowy rekord jest przekazywany jako tablica asocjacyjna lub obiekt iterowalny (taki jak `ArrayHash` używany w [formularzach |forms:]), gdzie klucze odpowiadają nazwom kolumn w tabeli. + +Jeśli tabela ma zdefiniowany klucz główny, metoda zwraca obiekt `ActiveRow`, który jest ponownie ładowany z bazy danych w celu odzwierciedlenia wszelkich zmian wprowadzonych na poziomie bazy danych (np. wyzwalaczy, domyślnych wartości kolumn lub obliczeń automatycznego zwiększania). Zapewnia to spójność danych, a obiekt zawsze zawiera aktualne dane bazy danych. Jeśli klucz główny nie jest jawnie zdefiniowany, metoda zwraca dane wejściowe jako tablicę. ```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 jest instancją ActiveRow zawierającą pełne dane wstawionego wiersza, +// w tym automatycznie wygenerowany identyfikator i wszelkie zmiany wprowadzone przez wyzwalacze. +echo $row->id; // Wyświetla identyfikator nowo wstawionego użytkownika +echo $row->created_at; // Wyświetla czas utworzenia, jeśli został ustawiony przez wyzwalacz ``` -Jeśli tabela ma zdefiniowany klucz główny, zwraca nowy wiersz jako obiekt ActiveRow. +**Wstawianie wielu rekordów jednocześnie:**. -Wkładka wielokrotna: +Metoda `insert()` umożliwia wstawianie wielu rekordów za pomocą jednego zapytania SQL. W tym przypadku zwraca liczbę wstawionych wierszy. ```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 będzie równe 2 ``` -Jako parametry możemy również przekazywać pliki lub obiekty DateTime: +Jako parametr można również przekazać obiekt `Selection` z wyborem danych. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Wstawianie wartości specjalnych:**. + +Wartości mogą zawierać pliki, obiekty `DateTime` lub literały SQL: ```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, // konwertuje do formatu bazy danych + 'avatar' => fopen('image.jpg', 'rb'), // wstawia zawartość pliku binarnego + 'uuid' => $explorer::literal('UUID()'), // wywołuje funkcję UUID() ]); ``` -Edycja rekordów (zwraca liczbę zmienionych wierszy): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Aktualizuje wiersze w tabeli na podstawie określonego filtra. Zwraca liczbę faktycznie zmodyfikowanych wierszy. + +Kolumny do aktualizacji są przekazywane jako tablica asocjacyjna lub obiekt iterowalny (taki jak `ArrayHash` używany w [formularzach |forms:]), gdzie klucze odpowiadają nazwom kolumn w tabeli: ```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 ``` -Do aktualizacji możemy użyć operatorów `+=` i `-=`: +Aby zmienić wartości liczbowe, można użyć operatorów `+=` i `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // všimněte si += + 'points+=' => 1, // zwiększa wartość kolumny "punkty" o 1 + 'coins-=' => 1, // zmniejsza wartość kolumny "coins" o 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Usuń rekordy (zwraca liczbę usuniętych wierszy): + +Selection::delete(): int .[method] +---------------------------------- + +Usuwa wiersze z tabeli na podstawie określonego filtra. Zwraca liczbę usuniętych wierszy. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Podczas wywoływania `update()` lub `delete()` należy użyć `where()` w celu określenia wierszy, które mają zostać zaktualizowane lub usunięte. Jeśli `where()` nie zostanie użyte, operacja zostanie wykonana na całej tabeli! + -Wiązania między tabelami .[#toc-working-with-relationships] -=========================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Aktualizuje dane w wierszu bazy danych reprezentowanym przez obiekt `ActiveRow`. Jako parametr przyjmuje dane iterowalne, gdzie kluczami są nazwy kolumn. Aby zmienić wartości liczbowe, można użyć operatorów `+=` i `-=`: -Sesja Ma jeden .[#toc-has-one-relation] ---------------------------------------- -Bardzo często spotykana jest sesja has one. Książka *ma jednego* autora. Książka *ma jednego* tłumacza. Wiersz, który znajduje się w relacji has one uzyskujemy za pomocą metody `ref()` Przyjmuje ona dwa argumenty: nazwę tabeli docelowej oraz nazwę kolumny złączenia. Zobacz przykład: +Po wykonaniu aktualizacji obiekt `ActiveRow` jest automatycznie przeładowywany z bazy danych, aby odzwierciedlić wszelkie zmiany wprowadzone na poziomie bazy danych (np. wyzwalacze). Metoda zwraca `true` tylko wtedy, gdy nastąpiła rzeczywista zmiana danych. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // zwiększa liczbę wyświetleń +]); +echo $article->views; // Wyświetla bieżącą liczbę wyświetleń ``` -W powyższym przykładzie wybieramy powiązanego autora z tabeli `author`. Klucz główny tabeli `author` jest wyszukiwany przez kolumnę `book.author_id`. Metoda `ref()` zwraca instancję `ActiveRow` lub `null`, jeśli poszukiwany rekord nie istnieje. Zwrócony wiersz jest instancją `ActiveRow`, więc możemy z nim pracować tak jak z rekordem książki. +Ta metoda aktualizuje tylko jeden określony wiersz w bazie danych. W przypadku zbiorczych aktualizacji wielu wierszy należy użyć metody [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Usuwa wiersz z bazy danych reprezentowany przez obiekt `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Usuwa książkę o identyfikatorze 1 +``` + +Metoda ta usuwa tylko jeden określony wiersz w bazie danych. Do masowego usuwania wielu wierszy należy użyć metody [Selection::delete() |#Selection::delete()]. -// nebo přímo -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +Relacje między tabelami .[#toc-relationships-between-tables] +============================================================ + +W relacyjnych bazach danych dane są podzielone na wiele tabel i połączone za pomocą kluczy obcych. Nette Database Explorer oferuje rewolucyjny sposób pracy z tymi relacjami - bez pisania zapytań JOIN lub wymagających jakiejkolwiek konfiguracji lub generowania encji. + +Do demonstracji użyjemy **przykładowej bazy danych**[(dostępnej na GitHub |https://github.com/nette-examples/books]). Baza danych zawiera następujące tabele: + +- `author` - autorzy i tłumacze (kolumny `id`, `name`, `web`, `born`) +- `book` - książki (kolumny `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - tagi (kolumny `id`, `name`) +- `book_tag` - tabela powiązań między książkami i tagami (kolumny `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Struktura bazy danych .<> + +W tym przykładzie bazy danych książek znajdujemy kilka rodzajów relacji (uproszczonych w porównaniu do rzeczywistości): + +- **One-to-many (1:N)** - Każda książka **ma jednego** autora; autor może napisać **wiele** książek. +- **Zero-do-wielu (0:N)** - Książka **może mieć** tłumacza; tłumacz może przetłumaczyć **wiele** książek. +- **Zero-do-jednego (0:1)** - książka **może mieć** sequel. +- **Many-to-many (M:N)** - książka **może mieć kilka** tagów, a tag może być przypisany do **wielu** książek. + +W tych relacjach zawsze istnieje **tabela nadrzędna** i **tabela podrzędna**. Na przykład, w relacji między autorami i książkami, tabela `author` jest rodzicem, a tabela `book` jest dzieckiem - można o tym myśleć jako o książce zawsze "należącej" do autora. Znajduje to również odzwierciedlenie w strukturze bazy danych: tabela podrzędna `book` zawiera klucz obcy `author_id`, który odwołuje się do tabeli nadrzędnej `author`. + +Jeśli chcemy wyświetlić książki wraz z nazwiskami ich autorów, mamy dwie opcje. Albo pobierzemy dane za pomocą pojedynczego zapytania SQL z JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Albo pobieramy dane w dwóch krokach - najpierw książki, potem ich autorów - i łączymy je w PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Książka ma też jednego tłumacza, łatwo możemy poznać jego nazwisko. +Drugie podejście jest, co zaskakujące, **bardziej wydajne**. Dane są pobierane tylko raz i mogą być lepiej wykorzystane w pamięci podręcznej. Dokładnie tak działa Nette Database Explorer - obsługuje wszystko pod maską i zapewnia czyste 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 jest rekordem z tabeli "author". + echo 'translated by: ' . $book->translator?->name; +} ``` -Takie podejście jest funkcjonalne, ale wciąż nieco uciążliwe, nie sądzisz? Baza danych zawiera już definicje kluczy obcych, więc dlaczego nie używać ich automatycznie. Spróbujmy. -Jeśli uzyskamy dostęp do zmiennej członkowskiej, która nie istnieje, ActiveRow spróbuje użyć nazwy tej zmiennej dla sesji "ma jedną". Odczytanie tej zmiennej jest takie samo jak wywołanie metody `ref()` z tylko jednym parametrem. Parametr ten będziemy nazywać **kluczem**. Ten klucz zostanie użyty do znalezienia klucza obcego w tabeli. Przekazany klucz jest porównywany z kolumnami, a jeśli pasuje do reguł, klucz obcy na tej kolumnie jest używany do odczytu danych z powiązanej tabeli. Zobacz przykład: +Dostęp do tabeli nadrzędnej .[#toc-accessing-the-parent-table] +-------------------------------------------------------------- + +Dostęp do tabeli nadrzędnej jest prosty. Są to relacje typu *książka ma autora* lub *książka może mieć tłumacza*. Dostęp do powiązanego rekordu można uzyskać za pośrednictwem właściwości obiektu `ActiveRow` - nazwa właściwości odpowiada nazwie kolumny klucza obcego bez przyrostka `id`: ```php -$book->author->name; -// to samo co -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // wyszukuje autora za pomocą kolumny "author_id +echo $book->translator?->name; // wyszukuje tłumacza za pomocą kolumny "translator_id ``` -Instancja ActiveRow nie ma kolumny `author`. Wszystkie kolumny tabeli `book` są skanowane w poszukiwaniu dopasowania do *klucza*. Dopasowanie w tym przypadku oznacza, że nazwa kolumny musi zawierać klucz. W powyższym przykładzie kolumna `author_id` zawiera ciąg "autor", a zatem pasuje do klucza "autor". Jeśli chcemy uzyskać dostęp do rekordu tłumacza, używamy klucza 'translator' w podobny sposób, ponieważ będzie on pasował do kolumny `translator_id` Więcej o logice dopasowywania kluczy można przeczytać w [Łączenie wyrażeń |#Filtering-by-Another-Table-Value]. +Podczas uzyskiwania dostępu do właściwości `$book->author` Explorer szuka kolumny w tabeli `book`, która zawiera ciąg `author` (tj. `author_id`). Na podstawie wartości w tej kolumnie pobiera odpowiedni rekord z tabeli `author` i zwraca go jako obiekt `ActiveRow`. Podobnie, `$book->translator` używa kolumny `translator_id`. Ponieważ kolumna `translator_id` może zawierać `null`, używany jest operator `?->`. + +Alternatywne podejście zapewnia metoda `ref()`, która przyjmuje dwa argumenty - nazwę tabeli docelowej i kolumnę łączącą - i zwraca instancję `ActiveRow` lub `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; // link do autora +echo $book->ref('author', 'translator_id')->name; // link do tłumacza ``` -Jeśli chcemy zdobyć autora wielu książek, stosujemy to samo podejście. Nette Database Explorer pobierze dla nas rekordy autora i tłumacza dla wszystkich książek naraz. +Metoda `ref()` jest przydatna, jeśli nie można użyć dostępu opartego na właściwościach, na przykład, gdy tabela zawiera kolumnę o tej samej nazwie co właściwość (`author`). W innych przypadkach korzystanie z dostępu opartego na właściwościach jest zalecane dla lepszej czytelności. + +Explorer automatycznie optymalizuje zapytania do bazy danych. Podczas przechodzenia przez książki i uzyskiwania dostępu do powiązanych z nimi rekordów (autorzy, tłumacze), Explorer nie generuje zapytania dla każdej książki z osobna. Zamiast tego wykonuje tylko **jedno zapytanie SELECT dla każdego typu relacji**, znacznie zmniejszając obciążenie bazy danych. Na przykład: ```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; } ``` -Ten kod wywoła tylko te trzy zapytania do bazy danych: +Ten kod wykona tylko trzy zoptymalizowane zapytania do bazy danych: + ```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 +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Logika identyfikacji kolumny łączącej jest definiowana przez implementację [Conventions |api:Nette\Database\Conventions]. Zalecamy użycie [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], która analizuje klucze obce i pozwala na płynną pracę z istniejącymi relacjami tabel. + -Sesja Ma wiele .[#toc-has-many-relation] ----------------------------------------- +Dostęp do tabeli podrzędnej .[#toc-accessing-the-child-table] +------------------------------------------------------------- -Sesja "ma wielu" jest po prostu odwrotnością sesji "ma jednego". Autor napisał kilka (*wiele*) książek. Autorka przetłumaczyła kilka (*wiele*) książek. Ten typ relacji jest trudniejszy, ponieważ relacja jest nazwana ("napisał", "przetłumaczył"). ActiveRow posiada metodę `related()`, która zwraca tablicę powiązanych rekordów. Rekordy są ponownie instancjami ActiveRow. Zobacz przykład: +Dostęp do tabeli podrzędnej działa w przeciwnym kierunku. Teraz pytamy *jakie książki napisał ten autor* lub *jakie książki przetłumaczył ten tłumacz*. Do tego typu zapytań używamy metody `related()`, która zwraca obiekt `Selection` z powiązanymi rekordami. Oto przykład: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' napsal:'; +$author = $explorer->table('author')->get(1); +// Wyświetla wszystkie książki napisane przez autora foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'a přeložil:'; +// Wyświetla wszystkie książki przetłumaczone przez autora foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Metoda `related()` przyjmuje opis połączenia jako dwa argumenty lub jako pojedynczy argument połączony kropką. Pierwszy argument to tabela docelowa, drugi to kolumna. +Metoda `related()` akceptuje opis relacji jako pojedynczy argument przy użyciu notacji kropkowej lub jako dwa oddzielne argumenty: ```php -$author->related('book.translator_id'); -// je stejné jako -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // pojedynczy argument +$author->related('book', 'translator_id'); // dwa argumenty ``` -Możemy wykorzystać heurystykę Nette Database Explorer opartą na kluczach obcych i użyć tylko **klucza**. Klucz zostanie porównany z kluczami obcymi, które odwołują się do bieżącej tabeli (tabela `author`). Jeśli zostanie znalezione dopasowanie, Nette Database Explorer użyje tego klucza obcego, w przeciwnym razie rzuci [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] lub [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Więcej o logice dopasowania kluczy można przeczytać w [Łączenie wyrażeń |#Filtering-by-Another-Table-Value]. +Explorer może automatycznie wykryć prawidłową kolumnę łączącą na podstawie nazwy tabeli nadrzędnej. W tym przypadku łączy przez kolumnę `book.author_id`, ponieważ nazwa tabeli źródłowej to `author`: -Oczywiście metodę `related()` można wywołać na wszystkich wyszukanych autorach, a Nette Database Explorer załaduje wszystkie pasujące książki jednocześnie. +```php +$author->related('book'); // uses book.author_id +``` + +Jeśli istnieje wiele możliwych połączeń, Explorer zgłosi wyjątek [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Oczywiście możemy również użyć metody `related()` podczas iteracji przez wiele rekordów w pętli, a Explorer automatycznie zoptymalizuje zapytania również w tym przypadku: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' napsal:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -W powyższym przykładzie uruchomiono tylko te dwa zapytania do bazy danych: +Ten kod generuje tylko dwa wydajne zapytania SQL: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors +``` + + +Relacja wiele do wielu .[#toc-many-to-many-relationship] +-------------------------------------------------------- + +W przypadku relacji wiele do wielu (M:N) wymagana jest **tabela łącząca** (w naszym przypadku `book_tag`). Tabela ta zawiera dwie kolumny klucza obcego (`book_id`, `tag_id`). Każda kolumna odwołuje się do klucza głównego jednej z połączonych tabel. Aby pobrać powiązane dane, najpierw pobieramy rekordy z tabeli łączącej za pomocą `related('book_tag')`, a następnie przechodzimy do danych docelowych: + +```php +$book = $explorer->table('book')->get(1); +// Wyświetla nazwy tagów przypisanych do książki +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // pobiera nazwę tagu poprzez tabelę linków +} + +$tag = $explorer->table('tag')->get(1); +// W przeciwnym kierunku: wyświetla tytuły książek z tym tagiem +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // pobiera tytuł książki +} +``` + +Explorer ponownie optymalizuje zapytania SQL do wydajnej postaci: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag ``` -Ręczny Eksplorator Tworzenia .[#toc-creating-explorer-manually] -=============================================================== +Zapytania poprzez powiązane tabele .[#toc-querying-through-related-tables] +-------------------------------------------------------------------------- -Jeśli utworzyliśmy połączenie z bazą danych za pomocą konfiguracji aplikacji, nie musimy się o nic martwić. Stworzyliśmy również usługę taką jak `Nette\Database\Explorer`, którą możemy przekazać za pomocą DI. +W metodach `where()`, `select()`, `order()` i `group()` można użyć specjalnych notacji, aby uzyskać dostęp do kolumn z innych tabel. Explorer automatycznie tworzy wymagane JOINy. -Jeśli jednak korzystamy z Nette Database Explorer osobno, musimy ręcznie utworzyć instancję `Nette\Database\Explorer`. +Notacja **Dot** (`parent_table.column`) jest używana dla relacji 1:N widzianych z perspektywy tabeli nadrzędnej: ```php -// $storage obsahuje implementację 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); +$books = $explorer->table('book'); + +// Wyszukuje książki, których nazwiska autorów zaczynają się na "Jon +$books->where('author.name LIKE ?', 'Jon%'); + +// Sortuje książki według nazwiska autora malejąco +$books->order('author.name DESC'); + +// Wyświetla tytuł książki i nazwisko autora +$books->select('book.title, author.name'); +``` + +**Notacja dwukropkowa** jest używana dla relacji 1:N z perspektywy tabeli nadrzędnej: + +```php +$authors = $explorer->table('author'); + +// Znajduje autorów, którzy napisali książkę z "PHP" w tytule +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Zlicza liczbę książek każdego autora +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +W powyższym przykładzie z notacją dwukropkową (`:book.title`), kolumna klucza obcego nie jest wyraźnie określona. Explorer automatycznie wykrywa prawidłową kolumnę na podstawie nazwy tabeli nadrzędnej. W tym przypadku łączy się przez kolumnę `book.author_id`, ponieważ nazwa tabeli źródłowej to `author`. Jeśli istnieje wiele możliwych połączeń, Explorer rzuca wyjątek [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Kolumna łącząca może być jawnie określona w nawiasach: + +```php +// Znajduje autorów, którzy przetłumaczyli książkę z "PHP" w tytule +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Notacje można łączyć w łańcuchy, aby uzyskać dostęp do danych w wielu tabelach: + +```php +// Znajduje autorów książek oznaczonych tagiem "PHP +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Warunki rozszerzające dla JOIN .[#toc-extending-conditions-for-join] +-------------------------------------------------------------------- + +Metoda `joinWhere()` dodaje dodatkowe warunki do złączeń tabel w SQL po słowie kluczowym `ON`. + +Załóżmy na przykład, że chcemy znaleźć książki przetłumaczone przez określonego tłumacza: + +```php +// Znajduje książki przetłumaczone przez tłumacza o imieniu "David +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +W warunku `joinWhere()` można użyć tych samych konstrukcji, co w metodzie `where()` - operatorów, symboli zastępczych, tablic wartości lub wyrażeń SQL. + +W przypadku bardziej złożonych zapytań z wieloma JOIN można zdefiniować aliasy tabel: + +```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) +``` + +Należy pamiętać, że podczas gdy metoda `where()` dodaje warunki do klauzuli `WHERE`, metoda `joinWhere()` rozszerza warunki w klauzuli `ON` podczas łączenia tabel. + + +Ręczne tworzenie eksploratora .[#toc-manually-creating-explorer] +================================================================ + +Jeśli nie korzystasz z kontenera Nette DI, możesz utworzyć instancję `Nette\Database\Explorer` ręcznie: + +```php +use Nette\Database; + +// $storage implementuje Nette\Caching\Storage, np: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// połączenie z bazą danych +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// zarządza odzwierciedleniem struktury bazy danych +$structure = new Database\Structure($connection, $storage); +// definiuje reguły mapowania nazw tabel, kolumn i kluczy obcych +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/pl/security.texy b/database/pl/security.texy new file mode 100644 index 0000000000..7b94a5ef0f --- /dev/null +++ b/database/pl/security.texy @@ -0,0 +1,145 @@ +Zagrożenia bezpieczeństwa +************************* + +.[perex] +Bazy danych często zawierają wrażliwe dane i pozwalają na niebezpieczne operacje. Nette Database zapewnia szereg funkcji bezpieczeństwa. Kluczowe jest jednak zrozumienie różnicy między bezpiecznymi i niebezpiecznymi interfejsami API. + + +SQL Injection .[#toc-sql-injection] +=================================== + +Wstrzyknięcie kodu SQL jest najpoważniejszym zagrożeniem bezpieczeństwa podczas pracy z bazami danych. Występuje, gdy niezaznaczone dane wejściowe użytkownika stają się częścią zapytania SQL. Atakujący może wstrzyknąć własne polecenia SQL, uzyskując lub modyfikując dane w bazie danych. + +```php +// NIEBEZPIECZNY KOD - podatny na wstrzyknięcie kodu SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Atakujący może wprowadzić coś takiego jak ' OR '1'='1 +// Wynikowym zapytaniem będzie: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// To zwróci wszystkich użytkowników! +``` + +To samo dotyczy Database Explorer: + +```php +// KOD NIEBEZPIECZEŃSTWA +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Bezpieczne sparametryzowane zapytania .[#toc-safe-parameterized-queries] +======================================================================== + +Bezpiecznym sposobem wstawiania wartości do zapytań SQL są zapytania parametryzowane. Nette Database zapewnia kilka sposobów ich użycia. + + +Znaki zapytania .[#toc-placeholder-question-marks] +-------------------------------------------------- + +Najprostszą metodą jest użycie znaków zastępczych: + +```php +// Bezpieczne sparametryzowane zapytania +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// Bezpieczny warunek w Eksploratorze +$table->where('name = ?', $_GET['name']); +``` + +To samo dotyczy wszystkich innych metod w Database Explorer, które umożliwiają wstawianie wyrażeń ze znakami zapytania i parametrami. + +.[warning] +Wartości muszą być typu skalarnego (`string`, `int`, `float`, `bool`) lub `null`. Jeśli np, `$_GET['name']` jest tablicą, Nette Database uwzględni wszystkie jej elementy w zapytaniu SQL, co może być niepożądane. + + +Tablice wartości .[#toc-value-arrays] +------------------------------------- + +W przypadku klauzul `INSERT`, `UPDATE` lub `WHERE` możemy użyć tablic wartości: + +```php +// Bezpieczny INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// Bezpieczne UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database automatycznie ucieka od wszystkich wartości przekazywanych przez sparametryzowane zapytania. Musimy jednak zapewnić prawidłowy typ danych parametrów. + + +Klucze tablicowe nie są bezpiecznym API .[#toc-array-keys-are-not-a-safe-api] +============================================================================= + +Podczas gdy wartości w tablicach są bezpieczne, tego samego nie można powiedzieć o kluczach: + +```php +// ❌ NIEBEZPIECZNY KOD - klucze mogą zawierać SQL injection +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +W przypadku poleceń `INSERT` i `UPDATE` jest to krytyczna luka w zabezpieczeniach - atakujący może wstawić lub zmodyfikować dowolną kolumnę w bazie danych. Na przykład, może ustawić `is_admin = 1` lub wstawić dowolne dane do wrażliwych kolumn. + +W warunkach `WHERE` jest to jeszcze bardziej niebezpieczne, ponieważ umożliwia **wyliczanie SQL** - technikę stopniowego pobierania informacji o bazie danych. Atakujący może próbować zbadać wynagrodzenia pracowników, wstrzykując dane do `$_GET` w ten sposób: + +```php +$_GET = ['salary >', 100000]; // rozpoczyna ustalanie przedziałów wynagrodzeń +``` + +Głównym problemem jest jednak to, że warunki `WHERE` obsługują wyrażenia SQL w kluczach: + +```php +// Legalne użycie operatorów w kluczach +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// NIEBEZPIECZNE: atakujący może wstrzyknąć własny SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // pozwala atakującemu uzyskać uprawnienia administratora +``` + +Jest to po raz kolejny **SQL injection**. + + +Kolumny na białej liście .[#toc-whitelisting-columns] +----------------------------------------------------- + +Jeśli chcesz zezwolić użytkownikom na wybór kolumn, zawsze używaj białej listy: + +```php +// Bezpieczne przetwarzanie - tylko dozwolone kolumny +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dynamiczne identyfikatory .[#toc-dynamic-identifiers] +===================================================== + +W przypadku dynamicznych nazw tabel i kolumn należy użyć symbolu zastępczego `?name`: + +```php +// Bezpieczne korzystanie z zaufanych identyfikatorów +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// UNSAFE - nigdy nie używaj danych wejściowych użytkownika +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Symbol `?name` powinien być używany tylko dla zaufanych wartości zdefiniowanych w kodzie aplikacji. W przypadku wartości dostarczonych przez użytkownika należy ponownie użyć białej listy. diff --git a/database/pt/@left-menu.texy b/database/pt/@left-menu.texy index b8685232c4..a21cd55675 100644 --- a/database/pt/@left-menu.texy +++ b/database/pt/@left-menu.texy @@ -4,3 +4,4 @@ Base de dados - [Explorador |Explorer] - [Reflexão |Reflection] - [Configuração |Configuration] +- [Riscos de segurança |security] diff --git a/database/pt/explorer.texy b/database/pt/explorer.texy index 43ff1303b7..0489d57a7d 100644 --- a/database/pt/explorer.texy +++ b/database/pt/explorer.texy @@ -3,548 +3,927 @@ Explorador de banco de dados
-O Nette Database Explorer simplifica significativamente a recuperação de dados do banco de dados sem a escrita de consultas SQL. +O Nette Database Explorer é uma camada avançada que simplifica significativamente a recuperação de dados do banco de dados sem a necessidade de escrever consultas SQL. -- utiliza consultas eficientes -- nenhum dado é transmitido desnecessariamente -- apresenta uma sintaxe elegante +- Trabalhar com dados é natural e fácil de entender +- Gera consultas SQL otimizadas que buscam apenas os dados necessários +- Oferece acesso fácil a dados relacionados sem a necessidade de escrever consultas JOIN +- Funciona imediatamente sem nenhuma configuração ou geração de entidades
-Para usar o Database Explorer, comece com uma tabela - ligue para `table()` em um objeto [api:Nette\Database\Explorer]. A maneira mais fácil de obter uma instância de objeto de contexto é [descrita aqui |core#Connection and Configuration], ou, no caso em que o Nette Database Explorer é usado como uma ferramenta autônoma, ele pode ser [criado manualmente |#Creating Explorer Manually]. +O Nette Database Explorer é uma extensão da camada de baixo nível [do Nette Database Core |core], que acrescenta uma conveniente abordagem orientada a objetos ao gerenciamento de bancos de dados. + +O trabalho com o Explorer começa com a chamada do método `table()` no objeto [api:Nette\Database\Explorer] (a forma de obtê-lo está [descrita aqui |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // nome da tabela db é 'livro'. +$books = $explorer->table('book'); // "book" é o nome da tabela ``` -A chamada retorna uma instância de objeto de [Seleção |api:Nette\Database\Table\Selection], que pode ser iterada para recuperar todos os livros. Cada item (uma linha) é representado por uma instância do [ActiveRow |api:Nette\Database\Table\ActiveRow] com dados mapeados para suas propriedades: +O método retorna um objeto [Selection |api:Nette\Database\Table\Selection], que representa uma consulta SQL. Métodos adicionais podem ser encadeados a esse objeto para filtragem e classificação de resultados. A consulta é montada e executada somente quando os dados são solicitados, por exemplo, por meio da iteração com `foreach`. Cada linha é representada por um objeto [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // produz a coluna "title" (título) + echo $book->author_id; // produz a coluna 'author_id' } ``` -A obtenção de apenas uma fila específica é feita pelo método `get()`, que retorna diretamente uma instância ActiveRow. +O Explorer simplifica muito o trabalho com [relacionamentos de tabela |#Vazby mezi tabulkami]. O exemplo a seguir mostra a facilidade com que podemos gerar dados de tabelas relacionadas (livros e seus autores). Observe que não é necessário escrever consultas JOIN; o Nette as gera para nós: ```php -$book = $explorer->table('book')->get(2); // devolve livro com id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // cria um JOIN para a tabela "author" (autor) +} ``` -Vamos dar uma olhada no caso de uso comum. Você precisa ir buscar livros e seus autores. É uma relação 1:N comum. A solução freqüentemente usada é buscar dados usando uma consulta SQL com joins de tabela. A segunda possibilidade é buscar dados separadamente, executar uma consulta para obter livros e depois obter um autor para cada livro por outra consulta (por exemplo, em seu ciclo foreach). Isto poderia ser facilmente otimizado para executar apenas duas consultas, uma para os livros e outra para os autores necessários - e esta é exatamente a maneira como o Nette Database Explorer o faz. +O Nette Database Explorer otimiza as consultas para obter o máximo de eficiência. O exemplo acima executa apenas duas consultas SELECT, independentemente do fato de processarmos 10 ou 10.000 livros. -Nos exemplos abaixo, trabalharemos com o esquema do banco de dados na figura. Há links OneHasMany (1:N) (autor do livro `author_id` e possível tradutor `translator_id`, que pode ser `null`) e ManyHasMany (M:N) link entre o livro e suas tags. +Além disso, o Explorer rastreia quais colunas são usadas no código e busca apenas essas colunas no banco de dados, economizando ainda mais desempenho. Esse comportamento é totalmente automático e adaptável. Se você modificar o código posteriormente para usar colunas adicionais, o Explorer ajustará automaticamente as consultas. Você não precisa configurar nada nem pensar em quais colunas serão necessárias - deixe isso para a Nette. -[Um exemplo, incluindo um esquema, é encontrado no GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Estrutura da base de dados utilizada nos exemplos .<> +Filtragem e classificação .[#toc-filtering-and-sorting] +======================================================= -O seguinte código lista o nome do autor de cada livro e todas as suas etiquetas. [Discutiremos em breve |#Working with relationships] como isto funciona internamente. +A classe `Selection` fornece métodos para filtragem e classificação de dados. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Adiciona uma condição WHERE. Várias condições são combinadas com AND +| `whereOr(array $conditions)` | Adiciona um grupo de condições WHERE combinadas por meio de OR +| `wherePrimary($value)` | Adiciona uma condição WHERE com base na chave primária +| `order($columns, ...$params)` | Define a classificação com ORDER BY +| `select($columns, ...$params)` | Especifica quais colunas devem ser buscadas +| `limit($limit, $offset = null)` | Limita o número de linhas (LIMIT) e, opcionalmente, define OFFSET +| `page($page, $itemsPerPage, &$total = null)` | Define a paginação +| `group($columns, ...$params)` | Agrupa as linhas (GROUP BY) +| `having($condition, ...$params)`| Adiciona uma condição HAVING para filtrar as linhas agrupadas -foreach ($books como $book) { - echo 'title: ' . $book->title; - echo 'escrito por: . $book->author->name; // $book->autor é linha da tabela 'autor'. +Os métodos podem ser encadeados (a chamada [interface fluente |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 é linha da tabela 'tag'. - } -} -``` +Esses métodos também permitem o uso de notações especiais para acessar [dados de tabelas relacionadas |#Dotazování přes související tabulky]. -Você ficará satisfeito com a eficiência com que a camada de banco de dados funciona. O exemplo acima faz um número constante de solicitações que se assemelham a este: -```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)) -``` +Escapes e identificadores .[#toc-escaping-and-identifiers] +---------------------------------------------------------- -Se você usar o [cache |caching:] (default on), nenhuma coluna será consultada desnecessariamente. Após a primeira consulta, o cache armazenará os nomes das colunas usadas e o Nette Database Explorer executará as consultas somente com as colunas necessárias: +Os métodos escapam automaticamente dos parâmetros e dos identificadores de citação (nomes de tabelas e colunas), evitando a injeção de SQL. Para garantir a operação adequada, algumas regras devem ser seguidas: -```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)) +- Escreva palavras-chave, nomes de funções, procedimentos, etc., em **maiúsculas**. +- Escrever nomes de colunas e tabelas em **minúsculas**. +- Sempre passe strings usando **parâmetros**. + +```php +where('name = ' . $name); // **DISASTER**: vulnerável à injeção de SQL +where('name LIKE "%search%"'); // **WRONG**: complica a citação automática +where('name LIKE ?', '%search%'); // **CORRECT**: valor passado como parâmetro + +where('name like ?', $name); // **WRONG**: gera: `name` `like` ? +where('name LIKE ?', $name); // **CORRECT**: gera: `nome` LIKE ? +where('LOWER(name) = ?', $value);// **CORRETO**: LOWER(`nome`) = ? ``` -Seleções .[#toc-selections] -=========================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Veja as possibilidades de filtragem e restrição de linhas [api:Nette\Database\Table\Selection]: +Filtra os resultados usando as condições WHERE. Sua força reside no tratamento inteligente de vários tipos de valores e na seleção automática de operadores SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Colar ONDE usar AND como cola se duas ou mais condições forem fornecidas -| `$table->whereOr($where)` | Definir ONDE usar OU como cola se duas ou mais condições forem fornecidas -| `$table->order($columns)` | Definir ORDEM POR, pode ser expressão `('column DESC, id DESC')` -| `$table->select($columns)` | Conjunto de colunas recuperadas, pode ser expressão `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Definir LIMITES e OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Permite a paginação -| `$table->group($columns)` | Set GROUP BY -| `$table->having($having)` | Set HAVING +Uso básico: -Podemos usar a chamada [interface fluente |nette:introduction-to-object-oriented-programming#fluent-interfaces], por exemplo, `$table->where(...)->order(...)->limit(...)`. Várias condições `where` ou `whereOr` são vinculadas pelo operador `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' +``` +Graças à detecção automática de operadores adequados, você não precisa lidar com casos especiais - a Nette lida com eles para você: -onde() .[#toc-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) +// O espaço reservado ? pode ser usado sem um operador: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -O Nette Database Explorer pode adicionar automaticamente os operadores necessários para os valores passados: +O método também lida corretamente com condições negativas e matrizes vazias: -.[language-php] -| `$table->where('field', $value)` | campo = $value -| `$table->where('field', null)` | campo IS NULL -| `$table->where('field > ?', $val)` | campo > $val -| `$table->where('field', [1, 2])` | campo IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 OU nome = 'Jon Snow' -| `$table->where('field', $explorer->table($tableName))` | campo 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 -- não encontra nada +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- encontra tudo +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- encontra tudo +// $table->where('NOT id ?', $ids); // AVISO: Essa sintaxe não é suportada +``` -Você pode fornecer o espaço reservado mesmo sem operador de coluna. Estas chamadas são as mesmas. +Você também pode passar o resultado de outra consulta de tabela como parâmetro, criando uma subconsulta: ```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')); ``` -Esta característica permite gerar um operador correto com base no valor: +As condições também podem ser passadas como uma matriz, com os itens combinados usando AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`preço_final` < `preço_original`) AND (`contagem_de_estoque` > `min_estoque`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -A seleção também trata corretamente as condições negativas, funciona também para matrizes vazias: +Na matriz, podem ser usados pares de valores-chave, e o Nette novamente escolherá automaticamente os operadores corretos: ```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], +]); +``` -// isto abrirá uma exceção, esta sintaxe não é suportada -$table->where('NOT id ?', $ids); +Também podemos misturar expressões SQL com placeholders e vários parâmetros. Isso é útil para condições complexas com operadores definidos com precisão: + +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // dois parâmetros são passados como uma matriz +]); ``` +Várias chamadas para `where()` combinam automaticamente as condições usando AND. + -ondeOr() .[#toc-whereor] ------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Exemplo de uso sem parâmetros: +Semelhante ao `where()`, mas combina condições usando 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, ]); ``` -Nós usamos os parâmetros. Se você não especificar um operador, o Nette Database Explorer adicionará automaticamente o apropriado: +Expressões mais complexas também podem ser usadas: ```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, ]); ``` -A chave pode conter uma expressão contendo pontos de interrogação de curinga e depois passar parâmetros no valor: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Adiciona uma condição para a chave primária da tabela: ```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]); +``` + +Se a tabela tiver uma chave primária composta (por exemplo, `foo_id`, `bar_id`), nós a passaremos como uma matriz: + +```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(); ``` -ordem() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Exemplos de uso: +Especifica a ordem em que as linhas são retornadas. Você pode classificar por uma ou mais colunas, em ordem crescente ou decrescente, ou por uma expressão personalizada: ```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 ``` -selecione() .[#toc-select] --------------------------- +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Exemplos de uso: +Especifica as colunas a serem retornadas do banco de dados. Por padrão, o Nette Database Explorer retorna apenas as colunas que são realmente usadas no código. Use o método `select()` quando precisar recuperar expressões específicas: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Os aliases definidos com o uso de `AS` podem ser acessados como propriedades do objeto `ActiveRow`: -limite() .[#toc-limit] ----------------------- +```php +foreach ($table as $row) { + echo $row->formatted_date; // acessar o alias +} +``` + + +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Exemplos de uso: +Limita o número de linhas retornadas (LIMIT) e, opcionalmente, define um deslocamento: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (retorna as 10 primeiras linhas) +$table->limit(10, 20); // LIMITE 10 DESLOCAMENTO 20 ``` +Para paginação, é mais apropriado usar o método `page()`. -página() .[#toc-page] ---------------------- -Uma maneira alternativa de estabelecer o limite e a compensação: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Simplifica a paginação dos resultados. Ele aceita o número da página (a partir de 1) e o número de itens por página. Opcionalmente, você pode passar uma referência a uma variável na qual o número total de páginas será armazenado: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Obtendo o número da última página, passado para a variável `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Agrupa as linhas pelas colunas especificadas (GROUP BY). Normalmente, é usado em combinação com funções de agregação: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Conta o número de produtos em cada categoria +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -grupo() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Exemplos de uso: +Define uma condição para filtragem de linhas agrupadas (HAVING). Pode ser usado em combinação com o método `group()` e as funções de agregação: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Localiza categorias com mais de 100 produtos +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -tendo() .[#toc-having] ----------------------- +Leitura de dados +================ -Exemplos de uso: +Para ler dados do banco de dados, há vários métodos úteis disponíveis: + +.[language-php] +| `foreach ($table as $key => $row)` | Itera por todas as linhas, `$key` é o valor da chave primária, `$row` é um objeto ActiveRow +| `$row = $table->get($key)` | Retorna uma única linha por chave primária +| `$row = $table->fetch()` | Retorna a linha atual e avança o ponteiro para a próxima +| `$array = $table->fetchPairs()` | Cria uma matriz associativa a partir dos resultados. +| `$array = $table->fetchAll()` | Retorna todas as linhas como um array +| `count($table)` | Retorna o número de linhas no objeto Selection + +O objeto [ActiveRow |api:Nette\Database\Table\ActiveRow] é somente leitura. Isso significa que você não pode alterar os valores de suas propriedades. Essa restrição garante a consistência dos dados e evita efeitos colaterais inesperados. Os dados são obtidos do banco de dados e todas as alterações devem ser feitas explicitamente e de forma controlada. + + +`foreach` - Iteração em todas as linhas +--------------------------------------- + +A maneira mais fácil de executar uma consulta e recuperar linhas é por meio da iteração com o loop `foreach`. Ele executa automaticamente a consulta SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = chave primária, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtragem por outro valor de tabela .[#toc-joining-key] -------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Executa uma consulta SQL e retorna uma linha por sua chave primária ou `null` se ela não existir. + +```php +$book = $explorer->table('book')->get(123); // Retorna ActiveRow com ID 123 ou nulo +if ($book) { + echo $book->title; +} +``` -Muitas vezes você precisa de resultados de filtragem por alguma condição que envolve outra tabela de banco de dados. Estes tipos de condição exigem a união de tabelas. Entretanto, você não precisa mais escrevê-las. -Digamos que você precisa obter todos os livros cujo nome do autor é 'Jon'. Tudo o que você precisa escrever é a chave de união da relação e o nome da coluna na tabela unida. A chave de união é derivada da coluna que se refere à tabela na qual você quer se juntar. Em nosso exemplo (veja o esquema db) é a coluna `author_id`, e é suficiente utilizar apenas a primeira parte dela - `author` (o sufixo `_id` pode ser omitido). `name` é uma coluna na tabela `author` que gostaríamos de utilizar. Uma condição para tradutor de livros (que é conectada pela coluna `translator_id` ) pode ser criada com a mesma facilidade. +fetch(): ?ActiveRow .[method] +----------------------------- + +Retorna uma linha e avança o ponteiro interno para a próxima. Se não houver mais linhas, ele retorna `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); +} ``` -A lógica da união é impulsionada pela implementação das [Convenções |api:Nette\Database\Conventions]. Incentivamos a utilização da [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analisa suas chaves estrangeiras e permite que você trabalhe facilmente com essas relações. -A relação entre o livro e seu autor é de 1:N. A relação inversa também é possível. Nós a chamamos de **backjoin***. Dê uma olhada em outro exemplo. Gostaríamos de ir buscar todos os autores, que escreveram mais de 3 livros. Para fazer o verso da união usamos `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY` declaração, também a condição tem que ser escrita na forma de `HAVING` declaração. +fetchPairs(): array .[method] +----------------------------- + +Retorna os resultados como uma matriz associativa. O primeiro argumento especifica o nome da coluna a ser usado como chave na matriz, e o segundo argumento especifica o nome da coluna a ser usado como valor: ```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', ...] ``` -Você deve ter notado que a expressão de adesão se refere ao livro, mas não está claro, se vamos aderir através de `author_id` ou `translator_id`. No exemplo acima, Selection se une através da coluna `author_id` porque foi encontrada uma correspondência com a tabela de origem - a tabela `author`. Se não houvesse tal correspondência e houvesse mais possibilidades, a Nette lançaria a [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Se apenas a coluna chave for especificada, o valor será a linha inteira, ou seja, o objeto `ActiveRow`: -Para fazer uma junção através da coluna `translator_id`, forneça um parâmetro opcional dentro da expressão de junção. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +Se `null` for especificado como a chave, a matriz será indexada numericamente a partir de zero: ```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', ...] ``` -Vamos dar uma olhada em alguma expressão de união mais difícil. +Você também pode passar um retorno de chamada como parâmetro, que retornará o próprio valor ou um par de valores-chave para cada linha. Se o retorno de chamada retornar apenas um valor, a chave será a chave primária da linha: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'First Book (Jan Novak)', ...] + +// A chamada de retorno também pode retornar uma matriz com um par de chave e valor: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['First Book' => 'Jan Novak', ...] +``` -Gostaríamos de encontrar todos os autores que tenham escrito algo sobre PHP. Todos os livros têm tags, então devemos selecionar os autores que escreveram qualquer livro com a tag PHP. + +fetchAll(): array .[method] +--------------------------- + +Retorna todas as linhas como uma matriz associativa de objetos `ActiveRow`, em que as chaves são os valores da chave primária. ```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, ...), ...] ``` -Consultas agregadas .[#toc-aggregate-queries] ---------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Obter número de filas -| `$table->count("DISTINCT $column")` | Obter número de valores distintos -| `$table->min($column)` | Obtenha um valor mínimo -| `$table->max($column)` | Obtenha o valor máximo -| `$table->sum($column)` | Obtenha a soma de todos os valores -| `$table->aggregation("GROUP_CONCAT($column)")` | Executar qualquer função de agregação +O método `count()` sem parâmetros retorna o número de linhas no objeto `Selection`: -.[caution] -O método `count()` sem nenhum parâmetro especificado seleciona todos os registros e retorna o tamanho da matriz, o que é muito ineficiente. Por exemplo, se você precisar calcular o número de linhas para paginação, especifique sempre o primeiro argumento. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativa +``` +Observação: `count()` com um parâmetro executa a função de agregação COUNT no banco de dados, conforme descrito abaixo. -Fuga e Cotação .[#toc-escaping-quoting] -======================================= -O Explorador de banco de dados é inteligente e tem parâmetros de escape e identificadores de citações para você. Estas regras básicas precisam ser seguidas, porém: +ActiveRow::toArray(): array .[method] +------------------------------------- -- palavras-chave, funções, procedimentos devem ser maiúsculas -- colunas e tabelas devem ser em letras minúsculas -- passar variáveis como parâmetros, não concatenar +Converte o objeto `ActiveRow` em uma matriz associativa em que as chaves são os nomes das colunas e os valores são os dados correspondentes. ```php -->where('name like ?', 'John'); // WRONG! gera: `name` `like` ? -->where('name LIKE ?', 'John'); // CORRETO +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray será ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + -->where('KEY = ?', $value); // ERRADO! KEY é uma palavra-chave -->where('key = ?', $value); // CORRET. gera: `key` = ? +Agregação .[#toc-aggregation] +============================= -->where('name = ' . $name); // WRONG! sql injection! -->where('name = ?', $name); // CORRETO! +A classe `Selection` fornece métodos para executar facilmente funções de agregação (COUNT, SUM, MIN, MAX, AVG, etc.). -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // WRONG! passar variáveis como parâmetros, não concatenar -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORRETO +.[language-php] +| `count($expr)` | Conta o número de linhas +| `min($expr)` | Retorna o valor mínimo em uma coluna +| `max($expr)` | Retorna o valor máximo em uma coluna +| `sum($expr)` | Retorna a soma dos valores em uma coluna +| `aggregation($function)` | Permite qualquer função de agregação, como `AVG()` ou `GROUP_CONCAT()` + + +count(string $expr): int .[method] +---------------------------------- + +Executa uma consulta SQL com a função COUNT e retorna o resultado. Esse método é usado para determinar quantas linhas correspondem a uma determinada condição: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `coluna`) FROM `tabela` +``` + +Observação: [count() |#count()] sem um parâmetro simplesmente retorna o número de linhas no objeto `Selection`. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Os métodos `min()` e `max()` retornam os valores mínimo e máximo na coluna ou expressão especificada: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Retorna a soma dos valores na coluna ou expressão especificada: + +```php +// SELECT SUM(`preço` * `itens_em_estoque`) FROM `produtos` WHERE `ativo` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -O uso errado pode produzir furos de segurança +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Permite a execução de qualquer função de agregação. -Obtenção de dados .[#toc-fetching-data] -======================================= +```php +// Calcula o preço médio dos produtos em uma categoria +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -| `foreach ($table as $id => $row)` | Iterate over all lines in result -| `$row = $table->get($id)` | Obtenha uma única linha com ID $id da tabela -| `$row = $table->fetch()` | Obtenha a próxima fileira do resultado -| `$array = $table->fetchPairs($key, $value)` | Buscar todos os valores para a matriz associativa -| `$array = $table->fetchPairs($value)` | Traga todas as filas para a matriz associativa -| `count($table)` | Obter o número de filas no conjunto de resultados +// Combina tags de produtos em uma única string +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Se precisarmos agregar resultados que resultem de uma agregação e agrupamento (por exemplo, `SUM(value)` sobre linhas agrupadas), especificaremos a função de agregação a ser aplicada a esses resultados intermediários como o segundo argumento: + +```php +// Calcula o preço total dos produtos em estoque para cada categoria e, em seguida, soma esses preços +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +Neste exemplo, primeiro calculamos o preço total dos produtos em cada categoria (`SUM(price * stock) AS category_total`) e agrupamos os resultados por `category_id`. Em seguida, usamos `aggregation('SUM(category_total)', 'SUM')` para somar esses subtotais. O segundo argumento `'SUM'` especifica a função de agregação a ser aplicada aos resultados intermediários. Inserir, atualizar e excluir .[#toc-insert-update-delete] ========================================================= -O método `insert()` aceita um conjunto de objetos Traversable (por exemplo, [ArrayHash |utils:arrays#ArrayHash] que devolve [formulários |forms:]): +O Nette Database Explorer simplifica a inserção, a atualização e a exclusão de dados. Todos os métodos mencionados lançam um `Nette\Database\DriverException` em caso de erro. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Insere novos registros em uma tabela. + +**Inserção de um único registro:** + +O novo registro é passado como uma matriz associativa ou objeto iterável (como `ArrayHash` usado em [formulários |forms:]), em que as chaves correspondem aos nomes das colunas na tabela. + +Se a tabela tiver uma chave primária definida, o método retornará um objeto `ActiveRow`, que é recarregado do banco de dados para refletir quaisquer alterações feitas no nível do banco de dados (por exemplo, acionadores, valores de coluna padrão ou cálculos de incremento automático). Isso garante a consistência dos dados e o objeto sempre contém os dados atuais do banco de dados. Se uma chave primária não for explicitamente definida, o método retornará os dados de entrada como uma matriz. ```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 é uma instância do ActiveRow que contém os dados completos da linha inserida, +// incluindo o ID gerado automaticamente e quaisquer alterações feitas por acionadores +echo $row->id; // Emite o ID do usuário recém-inserido +echo $row->created_at; // Emite a hora de criação, se definida por um acionador ``` -Se a chave primária estiver definida na tabela, um objeto ActiveRow contendo a linha inserida é devolvido. +**Inserção de vários registros de uma vez:** -Inserção múltipla: +O método `insert()` permite inserir vários registros com uma única consulta SQL. Nesse caso, ele retorna o número de linhas inseridas. ```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 será 2 +``` + +Você também pode passar um objeto `Selection` com uma seleção de dados como parâmetro. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Os arquivos ou objetos DateTime podem ser passados como parâmetros: +**Inserindo valores especiais:** + +Os valores podem incluir arquivos, objetos `DateTime` ou literais SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // converte para o formato de banco de dados + 'avatar' => fopen('image.jpg', 'rb'), // insere o conteúdo do arquivo binário + 'uuid' => $explorer::literal('UUID()'), // chama a função UUID() ]); ``` -Atualização (retorna a contagem das filas afetadas): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Atualiza as linhas em uma tabela com base em um filtro especificado. Retorna o número de linhas realmente modificadas. + +As colunas a serem atualizadas são passadas como uma matriz associativa ou objeto iterável (como `ArrayHash` usado em [formulários |forms:]), em que as chaves correspondem aos nomes das colunas na tabela: ```php -$count = $explorer->table('users') - ->where('id', 10) // must be called before 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 ``` -Para atualização, podemos utilizar os operadores `+=` a `-=`: +Para alterar valores numéricos, você pode usar os operadores `+=` e `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // aumenta o valor da coluna "pontos" em 1 + 'coins-=' => 1, // diminui o valor da coluna "coins" em 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Eliminação (retorna a contagem das linhas eliminadas): + +Selection::delete(): int .[method] +---------------------------------- + +Exclui linhas de uma tabela com base em um filtro especificado. Retorna o número de linhas excluídas. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Ao chamar `update()` ou `delete()`, certifique-se de usar `where()` para especificar as linhas a serem atualizadas ou excluídas. Se `where()` não for usado, a operação será executada em toda a tabela! + -Trabalhando com Relacionamentos .[#toc-working-with-relationships] -================================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Atualiza os dados em uma linha do banco de dados representada pelo objeto `ActiveRow`. Ele aceita dados iteráveis como parâmetro, em que as chaves são nomes de colunas. Para alterar valores numéricos, você pode usar os operadores `+=` e `-=`: -Tem Uma Relação .[#toc-has-one-relation] ----------------------------------------- -Tem uma relação é um caso de uso comum. O livro * tem um* autor. Livro *faz um* tradutor. A obtenção da linha relacionada é feita principalmente pelo método `ref()`. Aceita dois argumentos: nome da tabela de destino e coluna de junção de fonte. Veja o exemplo: +Após a atualização, o `ActiveRow` é automaticamente recarregado do banco de dados para refletir quaisquer alterações feitas no nível do banco de dados (por exemplo, acionadores). O método retorna `true` somente se tiver ocorrido uma alteração real nos dados. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // incrementa a contagem de visualizações +]); +echo $article->views; // Emite a contagem de visualizações atual ``` -No exemplo acima, buscamos a entrada do autor relacionado na tabela `author`, a chave primária do autor é pesquisada pela coluna `book.author_id`. O método Ref() retorna a instância ActiveRow ou nula se não houver entrada apropriada. A linha retornada é uma instância do ActiveRow para que possamos trabalhar com ela da mesma forma que com a entrada do livro. +Esse método atualiza apenas uma linha específica no banco de dados. Para atualizações em massa de várias linhas, use o método [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Exclui uma linha do banco de dados que é representada pelo objeto `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Exclui o livro com ID 1 +``` + +Esse método exclui apenas uma linha específica no banco de dados. Para a exclusão em massa de várias linhas, use o método [Selection::delete() |#Selection::delete()]. + -// or directly -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Relacionamentos entre tabelas .[#toc-relationships-between-tables] +================================================================== + +Nos bancos de dados relacionais, os dados são divididos em várias tabelas e conectados por meio de chaves estrangeiras. O Nette Database Explorer oferece uma maneira revolucionária de trabalhar com esses relacionamentos - sem escrever consultas JOIN ou exigir qualquer configuração ou geração de entidades. + +Para demonstração, usaremos o **banco de dados de exemplo**[(disponível no GitHub |https://github.com/nette-examples/books]). O banco de dados inclui as seguintes tabelas: + +- `author` - autores e tradutores (colunas `id`, `name`, `web`, `born`) +- `book` - livros (colunas `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - tags (colunas `id`, `name`) +- `book_tag` - tabela de links entre livros e tags (colunas `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Estrutura do banco de dados .<> + +Nesse exemplo de banco de dados de livros, encontramos vários tipos de relacionamentos (simplificados em comparação com a realidade): + +- **Um-para-muitos (1:N)** - Cada livro **tem um** autor; um autor pode escrever **múltiplos** livros. +- Zero-para-muitos (0:N)** - Um livro **pode ter** um tradutor; um tradutor pode traduzir **múltiplos** livros. +- Zero para um (0:1)** - Um livro **pode ter** uma sequência. +- Muitos para muitos (M:N)** - Um livro **pode ter várias** tags, e uma tag pode ser atribuída a **vários** livros. + +Nesses relacionamentos, há sempre uma **tabela pai** e uma **tabela filha**. Por exemplo, no relacionamento entre autores e livros, a tabela `author` é a tabela pai e a tabela `book` é a tabela filha - você pode pensar nisso como um livro sempre "pertencente" a um autor. Isso também se reflete na estrutura do banco de dados: a tabela filha `book` contém a chave estrangeira `author_id`, que faz referência à tabela pai `author`. + +Se quisermos exibir os livros junto com os nomes de seus autores, temos duas opções. Ou recuperamos os dados usando uma única consulta SQL com um JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Ou recuperamos os dados em duas etapas - primeiro os livros, depois seus autores - e os reunimos em PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -O livro também tem um tradutor, portanto, obter o nome do tradutor é bastante fácil. +A segunda abordagem é, surpreendentemente, **mais eficiente**. Os dados são obtidos apenas uma vez e podem ser mais bem utilizados no cache. É exatamente assim que o Nette Database Explorer funciona - ele cuida de tudo nos bastidores e fornece uma API limpa: + ```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 é um registro da tabela "author" (autor) + echo 'translated by: ' . $book->translator?->name; +} ``` -Tudo isso está bem, mas é um pouco incômodo, não acha? O Database Explorer já contém as definições de chaves estrangeiras, então por que não usá-las automaticamente? Vamos fazer isso! -Se chamarmos propriedade, que não existe, a ActiveRow tenta resolver o nome da propriedade chamadora como 'tem uma' relação. Obter esta propriedade é o mesmo que chamar o método ref() com apenas um argumento. Chamaremos o único argumento de **key***. A chave será resolvida para determinada relação de chave estrangeira. A chave passada é comparada com as colunas de linha, e se corresponder, a chave estrangeira definida na coluna correspondente é usada para obter dados da tabela de destino relacionada. Veja o exemplo: +Acesso à tabela pai .[#toc-accessing-the-parent-table] +------------------------------------------------------ + +O acesso à tabela pai é simples. Esses são relacionamentos como *um livro tem um autor* ou *um livro pode ter um tradutor*. O registro relacionado pode ser acessado por meio da propriedade de objeto `ActiveRow` - o nome da propriedade corresponde ao nome da coluna da chave estrangeira sem o sufixo `id`: ```php -$book->author->name; -// o mesmo que -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // encontra o autor por meio da coluna "author_id". +echo $book->translator?->name; // encontra o tradutor por meio da coluna "translator_id". ``` -A instância ActiveRow não tem coluna de autor. Todas as colunas de livros são pesquisadas para uma correspondência com *chave*. A correspondência, neste caso, significa que o nome da coluna tem que conter a chave. Assim, no exemplo acima, a coluna `author_id` contém a string 'autor' e, portanto, é correspondida pela chave 'autor'. Se você quiser obter o tradutor do livro, basta usar, por exemplo, 'tradutor' como chave, porque a chave 'tradutor' corresponderá à coluna `translator_id`. Você pode encontrar mais sobre a lógica de correspondência da chave no capítulo [Juntando expressões |#joining-key]. +Ao acessar a propriedade `$book->author`, o Explorer procura uma coluna na tabela `book` que contenha a string `author` (ou seja, `author_id`). Com base no valor dessa coluna, ele recupera o registro correspondente da tabela `author` e o retorna como um objeto `ActiveRow`. Da mesma forma, `$book->translator` usa a coluna `translator_id`. Como a coluna `translator_id` pode conter `null`, o operador `?->` é usado. + +Uma abordagem alternativa é fornecida pelo método `ref()`, que aceita dois argumentos - o nome da tabela de destino e a coluna de vinculação - e retorna uma instância `ActiveRow` ou `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; // link para o autor +echo $book->ref('author', 'translator_id')->name; // link para o tradutor ``` -Se você quiser ir buscar vários livros, você deve usar a mesma abordagem. O Nette Database Explorer buscará autores e tradutores para todos os livros buscados de uma só vez. +O método `ref()` é útil se o acesso baseado em propriedade não puder ser usado, por exemplo, quando a tabela contém uma coluna com o mesmo nome da propriedade (`author`). Em outros casos, recomenda-se usar o acesso baseado em propriedades para melhorar a legibilidade. + +O Explorer otimiza automaticamente as consultas ao banco de dados. Ao iterar pelos livros e acessar seus registros relacionados (autores, tradutores), o Explorer não gera uma consulta para cada livro individualmente. Em vez disso, ele executa apenas **uma consulta SELECT para cada tipo de relacionamento**, reduzindo significativamente a carga do banco de dados. Por exemplo: ```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; } ``` -O código executará apenas estas 3 consultas: +Este código executará apenas três consultas otimizadas ao banco de dados: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +A lógica para identificar a coluna de vinculação é definida pela implementação de [Conventions |api:Nette\Database\Conventions]. Recomendamos o uso do [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analisa chaves estrangeiras e permite que você trabalhe sem problemas com os relacionamentos de tabela existentes. -Tem muitas relações .[#toc-has-many-relation] ---------------------------------------------- -A relação "tem muitos" é apenas invertida "tem uma" relação. O autor * tem* escrito *many* livros. O autor * tem* traduzido *homens* livros. Como você pode ver, este tipo de relação é um pouco mais difícil porque a relação é 'nomeada' ('escrita', 'traduzida'). A instância ActiveRow tem o método `related()`, que retornará uma série de entradas relacionadas. As entradas também são instâncias do ActiveRow. Veja o exemplo abaixo: +Acesso à tabela filha .[#toc-accessing-the-child-table] +------------------------------------------------------- + +O acesso à tabela secundária funciona na direção oposta. Agora perguntamos *quais livros este autor escreveu* ou *quais livros este tradutor traduziu*. Para esse tipo de consulta, usamos o método `related()`, que retorna um objeto `Selection` com registros relacionados. Aqui está um exemplo: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Produz todos os livros escritos pelo autor foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Gera todos os livros traduzidos pelo autor foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Método `related()` método aceita a descrição completa de união passada como dois argumentos ou como um argumento unido por ponto. O primeiro argumento é a tabela de destino, o segundo é a coluna de destino. +O método `related()` aceita a descrição da relação como um único argumento usando a notação de ponto ou como dois argumentos separados: ```php -$author->related('book.translator_id'); -// o mesmo que -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // um único argumento +$author->related('book', 'translator_id'); // dois argumentos ``` -Você pode usar a heurística do Nette Database Explorer baseada em chaves estrangeiras e fornecer apenas **key*** argumento. A chave será comparada com todas as chaves estrangeiras que apontam para a tabela atual (`author` tabela). Se houver uma correspondência, o Nette Database Explorer utilizará esta chave estrangeira, caso contrário lançará [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] ou [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Você pode encontrar mais sobre a lógica de correspondência de chaves no capítulo [Junção de expressões |#joining-key]. +O Explorer pode detectar automaticamente a coluna de vinculação correta com base no nome da tabela pai. Nesse caso, ele faz a vinculação por meio da coluna `book.author_id` porque o nome da tabela de origem é `author`: -É claro que você pode chamar métodos relacionados para todos os autores buscados, o Nette Database Explorer buscará novamente os livros apropriados de uma só vez. +```php +$author->related('book'); // usa book.author_id +``` + +Se houver várias conexões possíveis, o Explorer lançará uma exceção [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +É claro que também podemos usar o método `related()` ao iterar por vários registros em um loop, e o Explorer também otimizará automaticamente as consultas nesse caso: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -O exemplo acima só vai dar duas consultas: +Esse código gera apenas duas consultas SQL eficientes: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Criação manual do Explorer .[#toc-creating-explorer-manually] -============================================================= +Relacionamento de muitos para muitos .[#toc-many-to-many-relationship] +---------------------------------------------------------------------- + +Para um relacionamento de muitos para muitos (M:N), é necessária uma **tabela de junção** (no nosso caso, `book_tag`). Essa tabela contém duas colunas de chave estrangeira (`book_id`, `tag_id`). Cada coluna faz referência à chave primária de uma das tabelas conectadas. Para recuperar os dados relacionados, primeiro buscamos os registros da tabela de ligação usando `related('book_tag')` e, em seguida, prosseguimos para os dados de destino: + +```php +$book = $explorer->table('book')->get(1); +// Emite os nomes das tags atribuídas ao livro +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // obtém o nome da tag por meio da tabela de links +} + +$tag = $explorer->table('tag')->get(1); +// Direção oposta: produz os títulos dos livros com essa tag +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // busca o título do livro +} +``` -Uma conexão de banco de dados pode ser criada usando a configuração da aplicação. Nesses casos, um serviço `Nette\Database\Explorer` é criado e pode ser passado como uma dependência usando o container DI. +O Explorer otimiza novamente as consultas SQL em um formato eficiente: -Entretanto, se o Nette Database Explorer for usado como uma ferramenta autônoma, uma instância de objeto `Nette\Database\Explorer` precisa ser criada manualmente. +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Consultas em tabelas relacionadas .[#toc-querying-through-related-tables] +------------------------------------------------------------------------- + +Nos métodos `where()`, `select()`, `order()` e `group()`, é possível usar notações especiais para acessar colunas de outras tabelas. O Explorer cria automaticamente os JOINs necessários. + +A notação **Dot** (`parent_table.column`) é usada para relacionamentos 1:N, conforme visto da perspectiva da tabela pai: ```php -// $storage implementa Nette\Caching\Storage -$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); +$books = $explorer->table('book'); + +// Encontra livros cujos nomes de autores começam com "Jon +$books->where('author.name LIKE ?', 'Jon%'); + +// Classifica os livros por nome de autor de forma decrescente +$books->order('author.name DESC'); + +// Gera o título do livro e o nome do autor +$books->select('book.title, author.name'); +``` + +**A notação de cólon** é usada para relacionamentos 1:N da perspectiva da tabela pai: + +```php +$authors = $explorer->table('author'); + +// Encontra autores que escreveram um livro com "PHP" no título +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Conta o número de livros de cada autor +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +No exemplo acima com a notação de dois pontos (`:book.title`), a coluna de chave estrangeira não é especificada explicitamente. O Explorer detecta automaticamente a coluna correta com base no nome da tabela pai. Nesse caso, ele se une por meio da coluna `book.author_id` porque o nome da tabela de origem é `author`. Se houver várias conexões possíveis, o Explorer lançará a exceção [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +A coluna de vinculação pode ser especificada explicitamente entre parênteses: + +```php +// Encontra autores que traduziram um livro com "PHP" no título +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +As notações podem ser encadeadas para acessar dados em várias tabelas: + +```php +// Encontra autores de livros marcados com "PHP +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Extensão das condições para JOIN .[#toc-extending-conditions-for-join] +---------------------------------------------------------------------- + +O método `joinWhere()` acrescenta condições adicionais às junções de tabelas no SQL após a palavra-chave `ON`. + +Por exemplo, digamos que queiramos encontrar livros traduzidos por um tradutor específico: + +```php +// Encontra livros traduzidos por um tradutor chamado 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN autor tradutor ON livro.tradutor_id = tradutor.id AND (tradutor.nome = 'David') +``` + +Na condição `joinWhere()`, você pode usar as mesmas construções do método `where()` - operadores, espaços reservados, matrizes de valores ou expressões SQL. + +Para consultas mais complexas com vários JOINs, podem ser definidos aliases de tabela: + +```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) +``` + +Observe que, enquanto o método `where()` adiciona condições à cláusula `WHERE`, o método `joinWhere()` estende as condições da cláusula `ON` durante as uniões de tabelas. + + +Criação manual do Explorer .[#toc-manually-creating-explorer] +============================================================= + +Se não estiver usando o contêiner Nette DI, você poderá criar uma instância do `Nette\Database\Explorer` manualmente: + +```php +use Nette\Database; + +// $storage implementa Nette\Caching\Storage, por exemplo: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// conexão com o banco de dados +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// gerencia a reflexão da estrutura do banco de dados +$structure = new Database\Structure($connection, $storage); +// define regras para mapear nomes de tabelas, colunas e chaves estrangeiras +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/pt/security.texy b/database/pt/security.texy new file mode 100644 index 0000000000..cbf79a8b35 --- /dev/null +++ b/database/pt/security.texy @@ -0,0 +1,145 @@ +Riscos de segurança +******************* + +.[perex] +Os bancos de dados geralmente contêm dados confidenciais e permitem operações perigosas. O Nette Database oferece vários recursos de segurança. No entanto, é fundamental entender a diferença entre APIs seguras e inseguras. + + +Injeção de SQL .[#toc-sql-injection] +==================================== + +A injeção de SQL é o risco de segurança mais grave quando se trabalha com bancos de dados. Ela ocorre quando uma entrada de usuário não verificada se torna parte de uma consulta SQL. Um invasor pode injetar seus próprios comandos SQL, obtendo ou modificando dados no banco de dados. + +```php +// CÓDIGO INSEGURO - vulnerável à injeção de SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// O invasor pode inserir algo como: ' OR '1'='1 +// A consulta resultante será: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Isso retorna todos os usuários! +``` + +O mesmo se aplica ao Database Explorer: + +```php +// CÓDIGO INSEGURO +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Consultas parametrizadas seguras .[#toc-safe-parameterized-queries] +=================================================================== + +A maneira segura de inserir valores em consultas SQL é por meio de consultas parametrizadas. O Nette Database oferece várias maneiras de usá-las. + + +Marcadores de interrogação de espaço reservado .[#toc-placeholder-question-marks] +--------------------------------------------------------------------------------- + +O método mais simples é usar pontos de interrogação de espaço reservado: + +```php +// Consultas parametrizadas seguras +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// Condição de segurança no Explorer +$table->where('name = ?', $_GET['name']); +``` + +O mesmo se aplica a todos os outros métodos do Database Explorer que permitem a inserção de expressões com pontos de interrogação e parâmetros de espaço reservado. + +.[warning] +Os valores devem ser do tipo escalar (`string`, `int`, `float`, `bool`) ou `null`. Se, por exemplo, `$_GET['name']` for uma matriz, o Nette Database incluirá todos os seus elementos na consulta SQL, o que pode ser indesejável. + + +Matrizes de valores .[#toc-value-arrays] +---------------------------------------- + +Nas cláusulas `INSERT`, `UPDATE` ou `WHERE`, podemos usar matrizes de valores: + +```php +// INSERT seguro +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// UPDATE seguro +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +O Nette Database escapa automaticamente de todos os valores passados por meio de consultas parametrizadas. No entanto, é preciso garantir o tipo de dados correto dos parâmetros. + + +As chaves de matriz não são uma API segura .[#toc-array-keys-are-not-a-safe-api] +================================================================================ + +Embora os valores em matrizes sejam seguros, o mesmo não se pode dizer das chaves: + +```php +// CÓDIGO INSEGURO - as chaves podem conter injeção de SQL +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Para os comandos `INSERT` e `UPDATE`, essa é uma falha de segurança crítica - um invasor pode inserir ou modificar qualquer coluna no banco de dados. Por exemplo, ele poderia definir `is_admin = 1` ou inserir dados arbitrários em colunas confidenciais. + +Nas condições do `WHERE`, isso é ainda mais perigoso porque permite a **enumeração do SQL**, uma técnica para recuperar gradualmente informações sobre o banco de dados. Um invasor poderia tentar explorar os salários dos funcionários injetando em `$_GET` dessa forma: + +```php +$_GET = ['salary >', 100000]; // começa a determinar as faixas salariais +``` + +O principal problema, entretanto, é que as condições do `WHERE` suportam expressões SQL nas chaves: + +```php +// Uso legítimo de operadores em chaves +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// INSEGURO: o invasor pode injetar seu próprio SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // permite que o invasor obtenha salários de administrador +``` + +Isso é mais uma vez **injeção de SQL**. + + +Colunas de lista branca .[#toc-whitelisting-columns] +---------------------------------------------------- + +Se quiser permitir que os usuários escolham colunas, use sempre uma lista de permissões: + +```php +// Processamento seguro - somente colunas permitidas +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Identificadores dinâmicos .[#toc-dynamic-identifiers] +===================================================== + +Para nomes dinâmicos de tabelas e colunas, use o espaço reservado `?name`: + +```php +// Uso seguro de identificadores confiáveis +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// UNSAFE - nunca use a entrada do usuário +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +O símbolo `?name` deve ser usado somente para valores confiáveis definidos no código do aplicativo. Para valores fornecidos pelo usuário, use uma lista branca novamente. diff --git a/database/ro/@left-menu.texy b/database/ro/@left-menu.texy index 9a3390befe..39322fd09b 100644 --- a/database/ro/@left-menu.texy +++ b/database/ro/@left-menu.texy @@ -4,3 +4,4 @@ Baza de date - [Explorator |Explorer] - [Reflecție |Reflection] - [Configurație |Configuration] +- [Riscuri de securitate |security] diff --git a/database/ro/explorer.texy b/database/ro/explorer.texy index 5756461c41..ada62dfbec 100644 --- a/database/ro/explorer.texy +++ b/database/ro/explorer.texy @@ -1,550 +1,929 @@ -Exploratorul de baze de date -**************************** +Explorator de baze de date +**************************
-Nette Database Explorer simplifică în mod semnificativ recuperarea datelor din baza de date fără a scrie interogări SQL. +Nette Database Explorer este un strat puternic care simplifică semnificativ recuperarea datelor din baza de date, fără a fi nevoie să scrieți interogări SQL. -- utilizează interogări eficiente -- nu se transmit date în mod inutil -- dispune de o sintaxă elegantă +- Lucrul cu datele este natural și ușor de înțeles +- Generează interogări SQL optimizate care extrag numai datele necesare +- Oferă acces ușor la date conexe fără a fi nevoie să scrieți interogări JOIN +- Funcționează imediat, fără nicio configurare sau generare de entități
-Pentru a utiliza Database Explorer, începeți cu un tabel - apelați `table()` pe un obiect [api:Nette\Database\Explorer]. Cel mai simplu mod de a obține o instanță de obiect contextual este [descris aici |core#Connection and Configuration] sau, pentru cazul în care Nette Database Explorer este utilizat ca instrument de sine stătător, acesta poate fi [creat manual |#Creating Explorer Manually]. +Nette Database Explorer este o extensie a stratului de nivel scăzut [Nette Database Core |core], care adaugă o abordare convenabilă orientată pe obiecte pentru gestionarea bazelor de date. + +Lucrul cu Explorer începe cu apelarea metodei `table()` pe obiectul [api:Nette\Database\Explorer] (modul de obținere a acestuia este [descris aici |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // numele tabelului db este 'book' +$books = $explorer->table('book'); // "book" este numele tabelului ``` -Apelul returnează o instanță a obiectului [Selection |api:Nette\Database\Table\Selection], care poate fi iterată pentru a prelua toate cărțile. Fiecare element (un rând) este reprezentat de o instanță de [ActiveRow |api:Nette\Database\Table\ActiveRow], cu date alocate proprietăților sale: +Metoda returnează un obiect [Selection |api:Nette\Database\Table\Selection], care reprezintă o interogare SQL. Metode suplimentare pot fi înlănțuite la acest obiect pentru filtrarea și sortarea rezultatelor. Interogarea este asamblată și executată numai atunci când datele sunt solicitate, de exemplu, prin iterarea cu `foreach`. Fiecare rând este reprezentat de un obiect [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // iese coloana "title" (titlu) + echo $book->author_id; // iese coloana "author_id } ``` -Obținerea unui singur rând specific se face prin metoda `get()`, care returnează direct o instanță ActiveRow. +Explorer simplifică foarte mult lucrul cu [relațiile dintre tabele |#Vazby mezi tabulkami]. Următorul exemplu arată cât de ușor putem extrage date din tabelele înrudite (cărți și autorii acestora). Observați că nu trebuie să scrieți nicio interogare JOIN; Nette le generează pentru noi: ```php -$book = $explorer->table('book')->get(2); // returnează cartea cu id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // creează un JOIN la tabelul "autor". +} ``` -Să aruncăm o privire la un caz de utilizare obișnuită. Aveți nevoie să obțineți cărți și autorii acestora. Este o relație comună 1:N. Soluția frecvent utilizată este de a prelua datele utilizând o singură interogare SQL cu îmbinări de tabele. A doua posibilitate este de a prelua datele separat, de a executa o interogare pentru a obține cărți și apoi de a obține un autor pentru fiecare carte printr-o altă interogare (de exemplu, în ciclul foreach). Acest lucru ar putea fi ușor de optimizat pentru a rula doar două interogări, una pentru cărți și alta pentru autorii necesari - și exact așa procedează Nette Database Explorer. +Nette Database Explorer optimizează interogările pentru eficiență maximă. Exemplul de mai sus efectuează doar două interogări SELECT, indiferent dacă procesăm 10 sau 10.000 de cărți. -În exemplele de mai jos, vom lucra cu schema bazei de date din figură. Există legături OneHasMany (1:N) (autorul cărții `author_id` și posibilul traducător `translator_id`, care poate fi `null`) și legături ManyHasMany (M:N) între carte și etichetele acesteia. +În plus, Explorer urmărește ce coloane sunt utilizate în cod și le extrage din baza de date numai pe acelea, economisind și mai multă performanță. Acest comportament este complet automat și adaptabil. Dacă modificați ulterior codul pentru a utiliza coloane suplimentare, Explorer ajustează automat interogările. Nu trebuie să configurați nimic sau să vă gândiți la coloanele care vor fi necesare - lăsați asta pe seama Nette. -[Un exemplu, inclusiv o schemă, se găsește pe GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Structura bazei de date utilizată în exemplele .<> +Filtrare și sortare .[#toc-filtering-and-sorting] +================================================= -Următorul cod enumeră numele autorului pentru fiecare carte și toate etichetele acesteia. Vom [discuta imediat |#Working with relationships] cum funcționează acest lucru la nivel intern. +Clasa `Selection` oferă metode pentru filtrarea și sortarea datelor. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Adaugă o condiție WHERE. Condițiile multiple sunt combinate folosind AND | +| `whereOr(array $conditions)` | Adaugă un grup de condiții WHERE combinate folosind OR | +| `wherePrimary($value)` | Adaugă o condiție WHERE bazată pe cheia primară | +| `order($columns, ...$params)` | Setează sortarea cu ORDER BY | +| `select($columns, ...$params)` | Specifică ce coloane se vor extrage | +| `limit($limit, $offset = null)` | Limitează numărul de rânduri (LIMIT) și stabilește opțional OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Setează paginarea | +| `group($columns, ...$params)` | Grupează rândurile (GROUP BY) | +| `having($condition, ...$params)`| Adaugă o condiție HAVING pentru filtrarea rândurilor grupate | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author este un rând din tabelul 'author' +Metodele pot fi înlănțuite (așa-numita [interfață fluentă |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 este un rând din tabelul 'tag' - } -} -``` +Aceste metode permit, de asemenea, utilizarea de notații speciale pentru accesarea [datelor din tabele conexe |#Dotazování přes související tabulky]. -Veți fi mulțumiți de cât de eficient funcționează stratul de bază de date. Exemplul de mai sus face un număr constant de cereri care arată astfel: -```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)) -``` +Escaping și identificatori .[#toc-escaping-and-identifiers] +----------------------------------------------------------- -Dacă folosiți [memoria cache |caching:] (activată în mod implicit), nicio coloană nu va fi interogată în mod inutil. După prima interogare, memoria cache va stoca numele coloanelor utilizate, iar Nette Database Explorer va rula interogări numai cu coloanele necesare: +Metodele scapă automat parametrii și identificatorii de citat (nume de tabele și coloane), împiedicând injectarea SQL. Pentru a asigura funcționarea corectă, trebuie respectate câteva reguli: -```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)) +- Scrieți cuvintele-cheie, numele funcțiilor, procedurilor etc. cu **cu majuscule**. +- Scrieți numele coloanelor și tabelelor cu **cifre mici**. +- Transmiteți întotdeauna șiruri de caractere folosind **parametri**. + +```php +where('name = ' . $name); // **DISASTER**: vulnerabil la injectarea SQL +where('name LIKE "%search%"'); // **WRONG**: complică citarea automată +where('name LIKE ?', '%search%'); // **CORRECT**: valoare transmisă ca parametru + +where('name like ?', $name); // **WRONG**: generează: `name` `like` ? +where('name LIKE ?', $name); // **CORECT**: generează: `name` LIKE ? +where('LOWER(name) = ?', $value);// **CORECT**: LOWER(`nume`) = ? ``` -Selecții .[#toc-selections] -=========================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Vedeți posibilitățile de filtrare și restricționare a rândurilor [api:Nette\Database\Table\Selection]: +Filtrează rezultatele utilizând condițiile WHERE. Punctul său forte constă în gestionarea inteligentă a diferitelor tipuri de valori și în selectarea automată a operatorilor SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Setați WHERE utilizând AND ca un liant dacă sunt furnizate două sau mai multe condiții -| `$table->whereOr($where)` | Set WHERE care utilizează OR ca liant dacă sunt furnizate două sau mai multe condiții. -| `$table->order($columns)` | Setați ORDER BY, poate fi o expresie `('column DESC, id DESC')` -| `$table->select($columns)` | Setați coloanele recuperate, poate fi o expresie `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Setați LIMIT și OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Activează paginarea -| `$table->group($columns)` | Setează GROUP BY -| `$table->having($having)` | Setează HAVING +Utilizare de bază: -Putem folosi o așa-numită [interfață fluentă |nette:introduction-to-object-oriented-programming#fluent-interfaces], de exemplu `$table->where(...)->order(...)->limit(...)`. Mai multe condiții `where` sau `whereOr` sunt legate între ele prin intermediul operatorului `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' +``` +Datorită detectării automate a operatorilor potriviți, nu trebuie să gestionați cazuri speciale - Nette le gestionează pentru dvs: -unde() .[#toc-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) +// Placeholder-ul ? poate fi utilizat fără operator: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer poate adăuga automat operatorii necesari pentru valorile transmise: +De asemenea, metoda gestionează corect condițiile negative și matricele goale: -.[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 -- nu găsește nimic +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- găsește totul +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- găsește totul +// $table->where('NOT id ?', $ids); // AVERTISMENT: Această sintaxă nu este acceptată +``` -Puteți furniza spații libere chiar și fără operatorul de coloană. Aceste apeluri sunt identice. +De asemenea, puteți trece rezultatul unei alte interogări de tabel ca parametru, creând o subinterogare: ```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')); ``` -Această caracteristică permite generarea operatorului corect pe baza valorii: +Condițiile pot fi, de asemenea, transmise sub formă de array, elementele fiind combinate folosind AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`preț_final` < `preț_original`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Selecția gestionează corect și condițiile negative, funcționează și pentru array-uri goale: +În matrice, pot fi utilizate perechi cheie-valoare, iar Nette va alege din nou automat operatorii corecți: ```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` = 'activ') AND (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` + +De asemenea, putem amesteca expresii SQL cu marcaje și parametri multipli. Acest lucru este util pentru condiții complexe cu operatori definiți cu precizie: -// acest lucru va genera o excepție, această sintaxă nu este acceptată. -$table->where('NOT id ?', $ids); +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // doi parametri sunt trecuți ca un array +]); ``` +Apelurile multiple la `where()` combină automat condițiile folosind AND. -whereOr() .[#toc-whereor] -------------------------- -Exemplu de utilizare fără parametri: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Similar cu `where()`, dar combină condițiile folosind 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, ]); ``` -Se utilizează parametrii. Dacă nu specificați un operator, Nette Database Explorer îl va adăuga automat pe cel corespunzător: +Pot fi utilizate și expresii mai complexe: ```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, ]); ``` -Cheia poate conține o expresie care să conțină semne de întrebare wildcard și apoi să treacă parametrii în valoare: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Adaugă o condiție pentru cheia primară a tabelului: ```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]); +``` + +Dacă tabelul are o cheie primară compusă (de exemplu, `foo_id`, `bar_id`), o transmitem sub formă de array: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Exemple de utilizare: +Specifică ordinea în care sunt returnate rândurile. Puteți sorta după una sau mai multe coloane, în ordine crescătoare sau descrescătoare, sau după o expresie personalizată: ```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() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Exemple de utilizare: +Specifică coloanele care urmează să fie returnate din baza de date. În mod implicit, Nette Database Explorer returnează numai coloanele care sunt utilizate efectiv în cod. Utilizați metoda `select()` atunci când trebuie să recuperați expresii specifice: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Aliasurile definite folosind `AS` sunt apoi accesibile ca proprietăți ale obiectului `ActiveRow`: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // accesați aliasul +} +``` -limit() .[#toc-limit] ---------------------- -Exemple de utilizare: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Limitează numărul de rânduri returnate (LIMIT) și, opțional, stabilește un offset: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (returnează primele 10 rânduri) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Pentru paginare, este mai adecvat să se utilizeze metoda `page()`. + -page() .[#toc-page] -------------------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -O modalitate alternativă de a seta limita și decalajul: +Simplifică paginarea rezultatelor. Acceptă numărul paginii (începând de la 1) și numărul de elemente pe pagină. Opțional, puteți trece o referință la o variabilă în care va fi stocat numărul total de pagini: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Obținerea ultimului număr de pagină, trecut în variabila `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Grupează rândurile după coloanele specificate (GROUP BY). Se utilizează de obicei în combinație cu funcții de agregare: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Numără numărul de produse din fiecare categorie +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Exemple de utilizare: +Stabilește o condiție pentru filtrarea rândurilor grupate (HAVING). Aceasta poate fi utilizată în combinație cu metoda `group()` și cu funcțiile de agregare: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Găsește categorii cu mai mult de 100 de produse +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Citirea datelor +=============== -Exemple de utilizare: +Pentru citirea datelor din baza de date, sunt disponibile mai multe metode utile: + +.[language-php] +| `foreach ($table as $key => $row)` | Iternează prin toate rândurile, `$key` este valoarea cheii primare, `$row` este un obiect ActiveRow | +| `$row = $table->get($key)` | Returnează un singur rând după cheia primară | +| `$row = $table->fetch()` | Returnează rândul curent și avansează pointerul la următorul rând | +| `$array = $table->fetchPairs()` | Creează un array asociativ din rezultate | +| `$array = $table->fetchAll()` | Returnează toate rândurile ca un array | +| `count($table)` | Returnează numărul de rânduri din obiectul Selection | + +Obiectul [ActiveRow |api:Nette\Database\Table\ActiveRow] este numai pentru citire. Aceasta înseamnă că nu puteți modifica valorile proprietăților sale. Această restricție asigură coerența datelor și previne efectele secundare neașteptate. Datele sunt preluate din baza de date, iar orice modificare trebuie efectuată explicit și într-un mod controlat. + + +`foreach` - Iterarea prin toate rândurile +----------------------------------------- + +Cel mai simplu mod de a executa o interogare și de a extrage rânduri este prin iterarea cu bucla `foreach`. Aceasta execută automat interogarea SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = cheie primară, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtrarea după o altă valoare de tabel .[#toc-joining-key] ----------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Execută o interogare SQL și returnează un rând după cheia sa primară sau `null` dacă nu există. + +```php +$book = $explorer->table('book')->get(123); // returnează ActiveRow cu ID 123 sau nul +if ($book) { + echo $book->title; +} +``` + -Destul de des aveți nevoie să filtrați rezultatele în funcție de o condiție care implică un alt tabel din baza de date. Aceste tipuri de condiții necesită îmbinarea tabelelor. Cu toate acestea, nu mai este nevoie să le scrieți. +fetch(): ?ActiveRow .[method] +----------------------------- -Să spunem că trebuie să obțineți toate cărțile al căror nume de autor este "Jon". Tot ce trebuie să scrieți este cheia de îmbinare a relației și numele coloanei din tabelul îmbinat. Cheia de îmbinare este derivată din coloana care se referă la tabelul pe care doriți să îl îmbinați. În exemplul nostru (a se vedea schema db), aceasta este coloana `author_id`, și este suficient să se utilizeze doar prima parte a acesteia - `author` (sufixul `_id` poate fi omis). `name` este o coloană din tabelul `author` pe care dorim să o utilizăm. O condiție pentru traducătorul de cărți (care este legată de coloana `translator_id` ) poate fi creată la fel de ușor. +Returnează un rând și avansează pointerul intern la următorul rând. Dacă nu mai există alte rânduri, se returnează `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); +} ``` -Logica cheilor de îmbinare este determinată de implementarea [Convențiilor |api:Nette\Database\Conventions]. Vă încurajăm să utilizați [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], care analizează cheile străine și vă permite să lucrați cu ușurință cu aceste relații. -Relația dintre carte și autorul acesteia este 1:N. Relația inversă este, de asemenea, posibilă. Noi o numim **backjoin**. Aruncați o privire la un alt exemplu. Dorim să obținem toți autorii care au scris mai mult de 3 cărți. Pentru a inversa îmbinarea, folosim instrucțiunea `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY`, iar condiția trebuie să fie scrisă sub forma unei instrucțiuni `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Returnează rezultatele sub forma unui array asociativ. Primul argument specifică numele coloanei care urmează să fie utilizată ca cheie în matrice, iar al doilea argument specifică numele coloanei care urmează să fie utilizată ca valoare: ```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", ...] ``` -Poate ați observat că expresia de alăturare se referă la carte, dar nu este clar dacă ne alăturăm prin `author_id` sau `translator_id`. În exemplul de mai sus, Selection se alătură prin coloana `author_id` deoarece a fost găsită o potrivire cu tabelul sursă - tabelul `author`. În cazul în care nu ar exista o astfel de potrivire și ar exista mai multe posibilități, Nette ar lansa [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Dacă este specificată doar coloana cheie, valoarea va fi întregul rând, adică obiectul `ActiveRow`: -Pentru a realiza o îmbinare prin intermediul coloanei `translator_id`, furnizați un parametru opțional în cadrul expresiei de îmbinare. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +Dacă `null` este specificat ca cheie, matricea va fi indexată numeric începând de la zero: ```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", ...] ``` -Să ne uităm la câteva expresii de îmbinare mai dificile. +De asemenea, puteți trece un callback ca parametru, care va returna fie valoarea în sine, fie o pereche cheie-valoare pentru fiecare rând. Dacă callback-ul returnează doar o valoare, cheia va fi cheia primară a rândului: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Prima carte (Jan Novak)', ...] + +// Callback-ul poate, de asemenea, să returneze un array cu o pereche cheie și valoare: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Prima carte' => 'Jan Novak', ...] +``` -Am dori să găsim toți autorii care au scris ceva despre PHP. Toate cărțile au etichete, deci ar trebui să selectăm acei autori care au scris o carte cu eticheta PHP. + +fetchAll(): array .[method] +--------------------------- + +Returnează toate rândurile ca o matrice asociativă de obiecte `ActiveRow`, unde cheile sunt valorile cheii primare. ```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, ...), ...] ``` -Interogări agregate .[#toc-aggregate-queries] ---------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Obțineți numărul de rânduri -| `$table->count("DISTINCT $column")` | Obține numărul de valori distincte -| `$table->min($column)` | Obține valoarea minimă -| `$table->max($column)` | Obține valoarea maximă -| `$table->sum($column)` | Obține suma tuturor valorilor -| `$table->aggregation("GROUP_CONCAT($column)")` | Rulați orice funcție de agregare +Metoda `count()` fără parametri returnează numărul de rânduri din obiectul `Selection`: -.[caution] -Metoda `count()` fără niciun parametru specificat selectează toate înregistrările și returnează dimensiunea tabloului, ceea ce este foarte ineficient. De exemplu, dacă trebuie să calculați numărul de rânduri pentru paginare, specificați întotdeauna primul argument. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativă +``` +Notă: `count()` cu un parametru efectuează funcția de agregare COUNT în baza de date, astfel cum este descris mai jos. -Evadare și citare .[#toc-escaping-quoting] -========================================== -Database Explorer este inteligent și evită parametrii și identificatorii de ghilimele pentru dumneavoastră. Totuși, trebuie respectate aceste reguli de bază: +ActiveRow::toArray(): array .[method] +------------------------------------- -- cuvintele cheie, funcțiile, procedurile trebuie să fie scrise cu majuscule -- coloanele și tabelele trebuie să fie scrise cu minuscule -- treceți variabilele ca parametri, nu concatenate +Convertește obiectul `ActiveRow` într-un array asociativ în care cheile sunt numele coloanelor, iar valorile sunt datele corespunzătoare. ```php -->where('name like ?', 'John'); // WRONG! generează: `name` `like` ? -->where('name LIKE ?', 'John'); // CORECT +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray va fi ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` -->where('KEY = ?', $value); // WRONG! KEY este un cuvânt cheie -->where('key = ?', $value); // CORECT. generează: `key` = ? -->where('name = ' . $name); // GREȘIT! injecție sql! -->where('name = ?', $name); // CORECT +Agregare .[#toc-aggregation] +============================ + +Clasa `Selection` oferă metode pentru realizarea cu ușurință a funcțiilor de agregare (COUNT, SUM, MIN, MAX, AVG, etc.). + +.[language-php] +| `count($expr)` | Numără numărul de rânduri | +| `min($expr)` | Returnează valoarea minimă dintr-o coloană | +| `max($expr)` | Returnează valoarea maximă a unei coloane | +| `sum($expr)` | Returnează suma valorilor dintr-o coloană | +| `aggregation($function)` | Permite orice funcție de agregare, cum ar fi `AVG()` sau `GROUP_CONCAT()` | -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // WRONG! treceți variabile ca parametri, nu concatenați -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // CORECT + +count(string $expr): int .[method] +---------------------------------- + +Execută o interogare SQL cu funcția COUNT și returnează rezultatul. Această metodă este utilizată pentru a determina câte rânduri corespund unei anumite condiții: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -.[warning] -Utilizarea greșită poate produce găuri de securitate +Notă: [count() |#count()] fără un parametru returnează pur și simplu numărul de rânduri din obiectul `Selection`. -Preluarea datelor .[#toc-fetching-data] -======================================= +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Metodele `min()` și `max()` returnează valorile minimă și maximă din coloana sau expresia specificată: + +```php +// SELECT MAX(`pret`) FROM `produse` WHERE `activ` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Returnează suma valorilor din coloana sau expresia specificată: + +```php +// SELECT SUM(`preț` * `articole_în_stock`) FROM `produse` WHERE `activ` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` + -| `foreach ($table as $id => $row)` | Iterați peste toate rândurile din rezultat -| `$row = $table->get($id)` | Obține un singur rând cu ID $id din tabel -| `$row = $table->fetch()` | Obține următorul rând din rezultat -| `$array = $table->fetchPairs($key, $value)` | Preluarea tuturor valorilor în matricea asociativă -| `$array = $table->fetchPairs($value)` | Preluarea tuturor rândurilor în matricea asociativă -| `count($table)` | Obține numărul de rânduri din setul de rezultate +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Permite executarea oricărei funcții de agregare. + +```php +// Calculează prețul mediu al produselor dintr-o categorie +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Combină etichetele produselor într-un singur șir +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Dacă trebuie să agregăm rezultate care sunt ele însele rezultatul unei agregări și grupări (de exemplu, `SUM(value)` peste rânduri grupate), specificăm funcția de agregare care urmează să fie aplicată acestor rezultate intermediare ca al doilea argument: + +```php +// Calculează prețul total al produselor din stoc pentru fiecare categorie, apoi însumează aceste prețuri +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +În acest exemplu, calculăm mai întâi prețul total al produselor din fiecare categorie (`SUM(price * stock) AS category_total`) și grupăm rezultatele prin `category_id`. Utilizăm apoi `aggregation('SUM(category_total)', 'SUM')` pentru a însuma aceste subtotaluri. Al doilea argument `'SUM'` specifică funcția de agregare care se aplică rezultatelor intermediare. Inserare, actualizare și ștergere .[#toc-insert-update-delete] ============================================================== -Metoda `insert()` acceptă o matrice de obiecte Traversable (de exemplu, [ArrayHash |utils:arrays#ArrayHash], care returnează [formulare |forms:]): +Nette Database Explorer simplifică inserarea, actualizarea și ștergerea datelor. Toate metodele menționate aruncă un `Nette\Database\DriverException` în cazul unei erori. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Introduce înregistrări noi într-un tabel. + +**Inserarea unei singure înregistrări:** + +Noua înregistrare este transmisă sub forma unui array asociativ sau a unui obiect iterabil (cum ar fi `ArrayHash` utilizat în [formulare |forms:]), unde cheile corespund numelor coloanelor din tabel. + +Dacă tabelul are o cheie primară definită, metoda returnează un obiect `ActiveRow`, care este reîncărcat din baza de date pentru a reflecta orice modificări efectuate la nivelul bazei de date (de exemplu, declanșatoare, valori implicite ale coloanelor sau calcule de autoincrementare). Acest lucru asigură coerența datelor, iar obiectul conține întotdeauna datele actuale din baza de date. Dacă nu este definită în mod explicit o cheie primară, metoda returnează datele de intrare sub forma unui array. ```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 este o instanță de ActiveRow care conține datele complete ale rândului inserat, +// inclusiv ID-ul autogenerat și orice modificări efectuate de declanșatoare +echo $row->id; // Afișează ID-ul utilizatorului nou introdus +echo $row->created_at; // Afișează ora creării, dacă a fost stabilită de un declanșator ``` -În cazul în care cheia primară este definită în tabel, se returnează un obiect ActiveRow care conține rândul inserat. +**Inserarea simultană a mai multor înregistrări:** -Inserare multiplă: +Metoda `insert()` vă permite să introduceți mai multe înregistrări cu o singură interogare SQL. În acest caz, se returnează numărul de rânduri introduse. ```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 va fi 2 ``` -Fișiere sau obiecte DateTime pot fi transmise ca parametri: +De asemenea, puteți trece ca parametru un obiect `Selection` cu o selecție de date. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Inserarea de valori speciale:** + +Valorile pot include fișiere, obiecte `DateTime` sau literale SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // sau $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserează fișierul + 'name' => 'John', + 'created_at' => new DateTime, // convertește în formatul bazei de date + 'avatar' => fopen('image.jpg', 'rb'), // inserează conținutul binar al fișierului + 'uuid' => $explorer::literal('UUID()'), // apelează la funcția UUID() ]); ``` -Actualizare (returnează numărul de rânduri afectate): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Actualizează rândurile dintr-un tabel pe baza unui filtru specificat. Returnează numărul de rânduri efectiv modificate. + +Coloanele care urmează să fie actualizate sunt transmise ca un array asociativ sau un obiect iterabil (cum ar fi `ArrayHash` utilizat în [formulare |forms:]), unde cheile corespund numelor coloanelor din tabel: ```php -$count = $explorer->table('users') - ->where('id', 10) // trebuie să fie apelat înainte de 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 ``` -Pentru actualizare se pot folosi operatorii `+=` a `-=`: +Pentru a modifica valorile numerice, puteți utiliza operatorii `+=` și `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // vezi += + 'points+=' => 1, // crește valoarea coloanei "puncte" cu 1 + 'coins-=' => 1, // scade valoarea coloanei "monede" cu 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Ștergere (returnează numărul de rânduri șterse): + +Selection::delete(): int .[method] +---------------------------------- + +Șterge rânduri dintr-un tabel pe baza unui filtru specificat. Returnează numărul de rânduri șterse. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Atunci când apelați `update()` sau `delete()`, asigurați-vă că utilizați `where()` pentru a specifica rândurile care urmează să fie actualizate sau șterse. Dacă nu se utilizează `where()`, operațiunea va fi efectuată pe întregul tabel! + -Lucrul cu relații .[#toc-working-with-relationships] -==================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Actualizează datele dintr-un rând din baza de date reprezentat de obiectul `ActiveRow`. Acesta acceptă date iterabile ca parametru, unde cheile sunt nume de coloane. Pentru a modifica valorile numerice, puteți utiliza operatorii `+=` și `-=`: -Are o singură relație .[#toc-has-one-relation] ----------------------------------------------- -Are o singură relație este un caz obișnuit de utilizare. Cartea *are un* autor. Cartea *are un* traducător. Obținerea rândului de relații se face în principal prin metoda `ref()`. Aceasta acceptă două argumente: numele tabelului țintă și coloana de legătură sursă. A se vedea exemplul: +După efectuarea actualizării, `ActiveRow` este reîncărcat automat din baza de date pentru a reflecta orice modificări efectuate la nivelul bazei de date (de exemplu, declanșatoare). Metoda returnează `true` numai dacă a avut loc o modificare reală a datelor. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // incrementează numărul de vizualizări +]); +echo $article->views; // Afișează numărul curent de vizualizări ``` -În exemplul de mai sus, preluăm intrarea autorului asociat din tabelul `author`, cheia primară a autorului este căutată prin coloana `book.author_id`. Metoda Ref() returnează o instanță ActiveRow sau este nulă dacă nu există o intrare corespunzătoare. Rândul returnat este o instanță de ActiveRow, astfel încât putem lucra cu el în același mod ca și cu intrarea de carte. +Această metodă actualizează doar un anumit rând din baza de date. Pentru actualizări masive ale mai multor rânduri, utilizați metoda [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Șterge un rând din baza de date care este reprezentat de obiectul `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Șterge cartea cu ID 1 +``` + +Această metodă șterge numai un anumit rând din baza de date. Pentru ștergerea în bloc a mai multor rânduri, utilizați metoda [Selection::delete() |#Selection::delete()]. + -// sau direct -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Relații între tabele .[#toc-relationships-between-tables] +========================================================= + +În bazele de date relaționale, datele sunt împărțite în mai multe tabele și conectate prin chei străine. Nette Database Explorer oferă un mod revoluționar de a lucra cu aceste relații - fără a scrie interogări JOIN sau a necesita vreo configurare sau generare de entități. + +Pentru demonstrație, vom utiliza **example database**[(disponibilă pe GitHub |https://github.com/nette-examples/books]). Baza de date include următoarele tabele: + +- `author` - autori și traducători (coloanele `id`, `name`, `web`, `born`) +- `book` - cărți (coloanele `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - etichete (coloanele `id`, `name`) +- `book_tag` - tabel de legături între cărți și etichete (coloanele `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Structura bazei de date .<> + +În acest exemplu de bază de date cu cărți, găsim mai multe tipuri de relații (simplificate față de realitate): + +- **One-to-many (1:N)** - Fiecare carte **are un** autor; un autor poate scrie **mai multe** cărți. +- **Zero-to-many (0:N)** - O carte **poate avea** un traducător; un traducător poate traduce **multiple** cărți. +- **Zero-to-one (0:1)** - O carte **poate avea** o continuare. +- **Many-to-many (M:N)** - O carte **poate avea mai multe** etichete, iar o etichetă poate fi atribuită la **mai multe** cărți. + +În aceste relații, există întotdeauna un tabel **parent** și un tabel **child**. De exemplu, în relația dintre autori și cărți, tabelul `author` este părintele, iar tabelul `book` este copilul - vă puteți gândi la aceasta ca la o carte care "aparține" întotdeauna unui autor. Acest lucru se reflectă și în structura bazei de date: tabelul copil `book` conține cheia externă `author_id`, care face trimitere la tabelul părinte `author`. + +Dacă dorim să afișăm cărțile împreună cu numele autorilor lor, avem două opțiuni. Fie recuperăm datele utilizând o singură interogare SQL cu un JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Cartea are, de asemenea, un traducător, astfel încât obținerea numelui traducătorului este destul de ușoară. +Fie recuperăm datele în două etape - mai întâi cărțile, apoi autorii acestora - și le asamblăm în PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books +``` + +A doua abordare este, surprinzător, **mai eficientă**. Datele sunt preluate o singură dată și pot fi utilizate mai bine în cache. Acesta este exact modul în care funcționează Nette Database Explorer - gestionează totul sub capotă și vă oferă un API curat: + ```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 este o înregistrare din tabelul "author + echo 'translated by: ' . $book->translator?->name; +} ``` -Toate acestea sunt bune, dar sunt oarecum greoaie, nu credeți? Database Explorer conține deja definițiile cheilor străine, așa că de ce să nu le folosim automat? Haideți să facem asta! -Dacă apelăm o proprietate, care nu există, ActiveRow încearcă să rezolve numele proprietății apelante ca fiind o relație "are o". Obținerea acestei proprietăți este identică cu apelarea metodei ref() cu un singur argument. Vom numi singurul argument **key**. Cheia va fi rezolvată în funcție de o anumită relație de cheie străină. Cheia transmisă este comparată cu coloanele rândului și, dacă se potrivește, cheia externă definită pe coloana corespunzătoare este utilizată pentru a obține date din tabelul țintă aferent. A se vedea exemplul: +Accesarea tabelului părinte .[#toc-accessing-the-parent-table] +-------------------------------------------------------------- + +Accesarea tabelului părinte este simplă. Acestea sunt relații precum *o carte are un autor* sau *o carte poate avea un traducător*. Înregistrarea conexă poate fi accesată prin intermediul proprietății obiectului `ActiveRow` - numele proprietății corespunde numelui coloanei cheii străine fără sufixul `id`: ```php -$book->author->name; -// la fel ca -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // găsește autorul prin intermediul coloanei "author_id +echo $book->translator?->name; // găsește traducătorul prin coloana "translator_id". ``` -Instanța ActiveRow nu are o coloană "author". Toate coloanele de cărți sunt căutate pentru a găsi o potrivire cu *key*. În acest caz, potrivirea înseamnă că numele coloanei trebuie să conțină cheia. Astfel, în exemplul de mai sus, coloana `author_id` conține șirul de caractere "author" și, prin urmare, se potrivește cu cheia "author". Dacă doriți să obțineți traducătorul cărții, puteți utiliza, de exemplu, "translator" ca cheie, deoarece cheia "translator" se va potrivi cu coloana `translator_id`. Puteți afla mai multe despre logica de potrivire a cheilor în capitolul [Expresii de îmbinare |#joining-key]. +Atunci când accesează proprietatea `$book->author`, Explorer caută o coloană în tabelul `book` care conține șirul `author` (de exemplu, `author_id`). Pe baza valorii din această coloană, Explorer extrage înregistrarea corespunzătoare din tabelul `author` și o returnează ca obiect `ActiveRow`. În mod similar, `$book->translator` utilizează coloana `translator_id`. Deoarece coloana `translator_id` poate conține `null`, se utilizează operatorul `?->`. + +O abordare alternativă este oferită de metoda `ref()`, care acceptă două argumente - numele tabelului țintă și coloana de legătură - și returnează o instanță `ActiveRow` sau `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; // link către autor +echo $book->ref('author', 'translator_id')->name; // link către traducător ``` -Dacă doriți să obțineți mai multe cărți, trebuie să utilizați aceeași abordare. Nette Database Explorer va prelua deodată autorii și traducătorii pentru toate cărțile preluate. +Metoda `ref()` este utilă în cazul în care accesul bazat pe proprietăți nu poate fi utilizat, de exemplu, atunci când tabelul conține o coloană cu același nume ca și proprietatea (`author`). În alte cazuri, este recomandată utilizarea accesului bazat pe proprietăți pentru o mai bună lizibilitate. + +Explorer optimizează automat interogările în baza de date. Atunci când iterați prin cărți și accesați înregistrările aferente acestora (autori, traducători), Explorer nu generează o interogare pentru fiecare carte în parte. În schimb, acesta execută doar **o singură interogare SELECT pentru fiecare tip de relație**, reducând semnificativ încărcarea bazei de date. De exemplu: ```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; } ``` -Codul va rula doar aceste 3 interogări: +Acest cod va executa doar trei interogări optimizate ale bazei de date: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Logica pentru identificarea coloanei de legătură este definită de implementarea [Conventions |api:Nette\Database\Conventions]. Vă recomandăm să utilizați [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], care analizează cheile străine și vă permite să lucrați fără probleme cu relațiile existente între tabele. -Are multe relații .[#toc-has-many-relation] -------------------------------------------- -Relația "are mai mulți" este doar o relație inversă a relației "are unul". Autorul *are* a scris *mai multe* cărți. Autorul *a* tradus *mai multe* cărți. După cum puteți vedea, acest tip de relație este puțin mai dificil, deoarece relația este "nominală" ("scris", "tradus"). Instanța ActiveRow are metoda `related()`, care va returna o matrice de intrări legate. Intrările sunt, de asemenea, instanțe ActiveRow. A se vedea exemplul de mai jos: +Accesarea tabelului copil .[#toc-accessing-the-child-table] +----------------------------------------------------------- + +Accesarea tabelului copilului funcționează în direcția opusă. Acum întrebăm *ce cărți a scris acest autor* sau *ce cărți a tradus acest traducător*. Pentru acest tip de interogare, folosim metoda `related()`, care returnează un obiect `Selection` cu înregistrările aferente. Iată un exemplu: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Afișează toate cărțile scrise de autor foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Afișează toate cărțile traduse de autor foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Metoda `related()` Metoda acceptă descrierea completă a îmbinării transmisă ca două argumente sau ca un singur argument unit prin punct. Primul argument este tabelul țintă, iar al doilea este coloana țintă. +Metoda `related()` acceptă descrierea relației ca un singur argument folosind notația punct sau ca două argumente separate: ```php -$author->related('book.translator_id'); -// la fel ca -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // un singur argument +$author->related('book', 'translator_id'); // două argumente ``` -Puteți utiliza euristica Nette Database Explorer bazată pe chei străine și să furnizați doar argumentul **key**. Cheia va fi comparată cu toate cheile străine care indică spre tabelul curent (`author` table). Dacă există o potrivire, [Nette |api:Nette\InvalidArgumentException] Database Explorer va utiliza această cheie externă, în caz contrar va arunca [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] sau [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Puteți afla mai multe despre logica de potrivire a cheilor în capitolul [Expresii de îmbinare |#joining-key]. +Explorer poate detecta automat coloana de legătură corectă pe baza numelui tabelului părinte. În acest caz, se face legătura prin coloana `book.author_id` deoarece numele tabelului sursă este `author`: -Desigur, puteți apela metodele aferente pentru toți autorii recuperați, Nette Database Explorer va recupera din nou cărțile corespunzătoare deodată. +```php +$author->related('book'); // utilizează book.author_id +``` + +Dacă există mai multe conexiuni posibile, Explorer va arunca o excepție [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Putem, desigur, să folosim metoda `related()` și atunci când iterăm prin mai multe înregistrări într-o buclă, iar Explorer va optimiza automat interogările și în acest caz: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Exemplul de mai sus va rula doar două interogări: +Acest cod generează doar două interogări SQL eficiente: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Crearea manuală a exploratorului .[#toc-creating-explorer-manually] -=================================================================== +Relația Many-to-Many .[#toc-many-to-many-relationship] +------------------------------------------------------ + +Pentru o relație mulți-la-mulți (M:N), este necesar un ** tabel de joncțiune** (în cazul nostru, `book_tag`). Acest tabel conține două coloane de chei străine (`book_id`, `tag_id`). Fiecare coloană face trimitere la cheia primară a unuia dintre tabelele conectate. Pentru a extrage datele legate, mai întâi extragem înregistrările din tabelul de legătură utilizând `related('book_tag')`, apoi continuăm cu datele țintă: + +```php +$book = $explorer->table('book')->get(1); +// Scoate numele etichetelor atribuite cărții +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // extrage numele etichetei prin intermediul tabelului de legături +} + +$tag = $explorer->table('tag')->get(1); +// Direcția opusă: afișează titlurile cărților cu această etichetă +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // extrage titlul cărții +} +``` + +Explorer optimizează din nou interogările SQL într-o formă eficientă: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + -O conexiune la baza de date poate fi creată folosind configurația aplicației. În astfel de cazuri, se creează un serviciu `Nette\Database\Explorer` care poate fi trecut ca dependență cu ajutorul containerului DI. +Interogarea prin tabele conexe .[#toc-querying-through-related-tables] +---------------------------------------------------------------------- -Cu toate acestea, în cazul în care Nette Database Explorer este utilizat ca instrument independent, trebuie creată manual o instanță a obiectului `Nette\Database\Explorer`. +În metodele `where()`, `select()`, `order()`, și `group()`, puteți utiliza notații speciale pentru a accesa coloane din alte tabele. Explorer creează automat JOIN-urile necesare. + +Notația **Dot** (`parent_table.column`) este utilizată pentru relațiile 1:N, văzute din perspectiva tabelului părinte: ```php -// $storage implements Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Găsește cărți ale căror nume de autori încep cu "Jon +$books->where('author.name LIKE ?', 'Jon%'); + +// Sortează cărțile după numele autorului descrescător +$books->order('author.name DESC'); + +// Scoate titlul cărții și numele autorului +$books->select('book.title, author.name'); +``` + +**Notația coloanelor** se utilizează pentru relațiile 1:N din perspectiva tabelului părinte: + +```php +$authors = $explorer->table('author'); + +// Găsește autorii care au scris o carte cu "PHP" în titlu +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Numără numărul de cărți pentru fiecare autor +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +În exemplul de mai sus cu notația două puncte (`:book.title`), coloana cheie externă nu este specificată explicit. Explorer detectează automat coloana corectă pe baza numelui tabelului părinte. În acest caz, se conectează prin coloana `book.author_id` deoarece numele tabelului sursă este `author`. Dacă există mai multe conexiuni posibile, Explorer aruncă excepția [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Coloana de legătură poate fi specificată explicit în paranteze: + +```php +// Găsește autori care au tradus o carte cu "PHP" în titlu +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Notațiile pot fi înlănțuite pentru a accesa date din mai multe tabele: + +```php +// Găsește autori de cărți etichetate cu "PHP +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Extinderea condițiilor pentru JOIN .[#toc-extending-conditions-for-join] +------------------------------------------------------------------------ + +Metoda `joinWhere()` adaugă condiții suplimentare la îmbinările de tabele în SQL după cuvântul-cheie `ON`. + +De exemplu, să spunem că dorim să găsim cărți traduse de un anumit traducător: + +```php +// Găsește cărți traduse de un traducător numit "David +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +În condiția `joinWhere()`, puteți utiliza aceleași construcții ca și în metoda `where()` - operatori, marcatori de poziție, matrici de valori sau expresii SQL. + +Pentru interogări mai complexe cu JOIN-uri multiple, se pot defini aliasuri de tabel: + +```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) +``` + +Rețineți că, în timp ce metoda `where()` adaugă condiții la clauza `WHERE`, metoda `joinWhere()` extinde condițiile din clauza `ON` în timpul îmbinărilor de tabele. + + +Crearea manuală a exploratorului .[#toc-manually-creating-explorer] +=================================================================== + +Dacă nu utilizați containerul Nette DI, puteți crea manual o instanță a `Nette\Database\Explorer`: + +```php +use Nette\Database; + +// $storage implementează Nette\Caching\Storage, de ex: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// conexiune la baza de date +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// gestionează reflectarea structurii bazei de date +$structure = new Database\Structure($connection, $storage); +// definește reguli pentru maparea numelor de tabele, coloanelor și cheilor străine +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/ro/security.texy b/database/ro/security.texy new file mode 100644 index 0000000000..6582e9a109 --- /dev/null +++ b/database/ro/security.texy @@ -0,0 +1,145 @@ +Riscuri de securitate +********************* + +.[perex] +Bazele de date conțin adesea date sensibile și permit operațiuni periculoase. Nette Database oferă o serie de caracteristici de securitate. Cu toate acestea, este esențial să înțelegeți diferența dintre API-urile sigure și cele nesigure. + + +Injecție SQL .[#toc-sql-injection] +================================== + +Injecția SQL este cel mai grav risc de securitate atunci când se lucrează cu baze de date. Aceasta are loc atunci când o intrare necontrolată a utilizatorului devine parte a unei interogări SQL. Un atacator își poate injecta propriile comenzi SQL, obținând sau modificând date din baza de date. + +```php +// ❌ UNSAFE CODE - vulnerabil la injectarea SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Atacatorul poate introduce ceva de genul: ' OR '1'='1 +// Interogarea rezultată va fi: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Aceasta returnează toți utilizatorii! +``` + +Același lucru este valabil și pentru Database Explorer: + +```php +// ❌ CODUL UNSAFE +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Interogări parametrizate sigure .[#toc-safe-parameterized-queries] +================================================================== + +Modul sigur de a introduce valori în interogările SQL este prin interogări parametrizate. Nette Database oferă mai multe modalități de utilizare a acestora. + + +Semne de întrebare de tip Placeholder .[#toc-placeholder-question-marks] +------------------------------------------------------------------------ + +Cea mai simplă metodă este utilizarea semnelor de întrebare: + +```php +// ✅ Interogări parametrizate sigure +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Condiție sigură în Explorer +$table->where('name = ?', $_GET['name']); +``` + +Același lucru este valabil pentru toate celelalte metode din Database Explorer care permit inserarea de expresii cu semne de întrebare și parametri. + +.[warning] +Valorile trebuie să fie de tip scalar (`string`, `int`, `float`, `bool`) sau `null`. Dacă, de exemplu, `$_GET['name']` este un array, Nette Database va include toate elementele sale în interogarea SQL, ceea ce poate fi nedorit. + + +Array-uri de valori .[#toc-value-arrays] +---------------------------------------- + +Pentru clauzele `INSERT`, `UPDATE`, sau `WHERE`, putem utiliza matrice de valori: + +```php +// ✅ INSERTARE sigură +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ UPDATE sigur +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database evadează automat toate valorile trecute prin interogări parametrizate. Cu toate acestea, trebuie să ne asigurăm de tipul corect de date al parametrilor. + + +Cheile array nu sunt un API sigur .[#toc-array-keys-are-not-a-safe-api] +======================================================================= + +În timp ce valorile din array-uri sunt sigure, nu același lucru se poate spune despre chei: + +```php +// ❌ UNSAFE CODE - cheile pot conține o injecție SQL +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Pentru comenzile `INSERT` și `UPDATE`, acesta este un defect de securitate critic - un atacator ar putea introduce sau modifica orice coloană din baza de date. De exemplu, ar putea seta `is_admin = 1` sau introduce date arbitrare în coloane sensibile. + +În condițiile `WHERE`, acest lucru este și mai periculos, deoarece permite **SQL enumeration** - o tehnică de a extrage treptat informații despre baza de date. Un atacator ar putea încerca să exploreze salariile angajaților injectând în `$_GET` astfel: + +```php +$_GET = ['salary >', 100000]; // începe să stabilească intervalele salariale +``` + +Cu toate acestea, principala problemă este că condițiile `WHERE` acceptă expresii SQL în chei: + +```php +// Utilizarea legitimă a operatorilor în chei +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ INSEGURĂ: atacatorul își poate injecta propriul SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // permite atacatorului să obțină salariile administratorilor +``` + +Aceasta este încă o dată **injecție SQL**. + + +Lista albă a coloanelor .[#toc-whitelisting-columns] +---------------------------------------------------- + +Dacă doriți să permiteți utilizatorilor să aleagă coloanele, utilizați întotdeauna o listă albă: + +```php +// ✅ Procesare sigură - numai coloane permise +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Identificatori dinamici .[#toc-dynamic-identifiers] +=================================================== + +Pentru numele dinamice ale tabelelor și coloanelor, utilizați marcajul `?name`: + +```php +// ✅ Utilizarea în siguranță a identificatorilor de încredere +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - nu utilizați niciodată date introduse de utilizator +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Simbolul `?name` ar trebui utilizat numai pentru valorile de încredere definite în codul aplicației. Pentru valorile furnizate de utilizator, utilizați din nou o listă albă. diff --git a/database/ru/@left-menu.texy b/database/ru/@left-menu.texy index 5b8fe06b0c..cd9d0624cf 100644 --- a/database/ru/@left-menu.texy +++ b/database/ru/@left-menu.texy @@ -4,3 +4,4 @@ - [Explorer] - [Размышление |Reflection] - [Настройка|configuration] +- [Риски безопасности |security] diff --git a/database/ru/explorer.texy b/database/ru/explorer.texy index cad0b7b939..2aae918793 100644 --- a/database/ru/explorer.texy +++ b/database/ru/explorer.texy @@ -1,550 +1,929 @@ -Database Explorer -***************** +Проводник базы данных +*********************
-Nette Database Explorer значительно упрощает получение данных из базы данных без написания SQL-запросов. +Nette Database Explorer - это мощный слой, который значительно упрощает получение данных из базы данных без необходимости написания SQL-запросов. -- использует эффективные запросы -- данные не передаются без необходимости -- отличается элегантным синтаксисом +- Работа с данными становится естественной и понятной +- Генерирует оптимизированные SQL-запросы, которые извлекают только необходимые данные +- Обеспечивает легкий доступ к связанным данным без необходимости написания JOIN-запросов +- Работает сразу, без какой-либо настройки или создания сущностей
-Чтобы использовать Database Explorer, начните с таблицы — вызовите `table()` на объекте [api:Nette\Database\Explorer]. Проще всего получить экземпляр контекстного объекта [описано здесь |core#Connection-and-Configuration], или, в случае, когда Nette Database Explorer используется как отдельный инструмент, его можно [создать вручную|#Creating-Explorer-Manually]. +Nette Database Explorer - это расширение низкоуровневого слоя [Nette Database Core |core], которое добавляет удобный объектно-ориентированный подход к управлению базами данных. + +Работа с Explorer начинается с вызова метода `table()` на объекте [api:Nette\Database\Explorer] (как его получить, [описано здесь |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // имя таблицы в бд — 'book' +$books = $explorer->table('book'); // 'book' - это имя таблицы ``` -Вызов возвращает экземпляр объекта [Selection |api:Nette\Database\Table\Selection], который можно итерировать для получения всех книг. Каждый элемент (строка) представлен экземпляром [ActiveRow |api:Nette\Database\Table\ActiveRow] с данными, отображенными в его свойствах: +Метод возвращает объект [Selection |api:Nette\Database\Table\Selection], который представляет собой SQL-запрос. К этому объекту можно подключить дополнительные методы для фильтрации и сортировки результатов. Запрос собирается и выполняется только при запросе данных, например, при итерации с помощью `foreach`. Каждая строка представлена объектом [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // выводит столбец 'title' + echo $book->author_id; // выводит колонку 'author_id' } ``` -Получение только одного конкретного ряда осуществляется методом `get()`, который непосредственно возвращает экземпляр ActiveRow. +Explorer значительно упрощает работу со [связями таблиц |#Vazby mezi tabulkami]. Следующий пример показывает, как легко мы можем вывести данные из связанных таблиц (книги и их авторы). Обратите внимание, что никаких JOIN-запросов писать не нужно - Nette генерирует их за нас: ```php -$book = $explorer->table('book')->get(2); // возвращает книгу с идентификатором 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // создает JOIN с таблицей 'author' +} ``` -Давайте рассмотрим распространенный случай использования. Вам нужно получить книги и их авторов. Это обычное отношение 1:N. Часто используемым решением является получение данных с помощью одного SQL-запроса с объединением таблиц. Вторая возможность — получить данные отдельно, выполнить один запрос для получения книг, а затем получить автора для каждой книги другим запросом (например, в цикле foreach). Это можно легко оптимизировать для выполнения только двух запросов, один для книг, а другой для нужных авторов — и именно так это делает Nette Database Explorer. +Nette Database Explorer оптимизирует запросы для достижения максимальной эффективности. В приведенном выше примере выполняется всего два запроса SELECT, независимо от того, обрабатываем ли мы 10 или 10 000 книг. -В приведенных ниже примерах мы будем работать со схемой базы данных, показанной на рисунке. Имеются связи OneHasMany (1:N) (автор книги `author_id` и возможный переводчик `translator_id`, который может быть `null`) и связи ManyHasMany (M:N) между книгой и её тегами. +Кроме того, Explorer отслеживает, какие столбцы используются в коде, и извлекает из базы данных только их, что еще больше снижает производительность. Это поведение полностью автоматическое и адаптивное. Если впоследствии вы измените код, чтобы использовать дополнительные столбцы, Explorer автоматически скорректирует запросы. Вам не нужно ничего настраивать или думать о том, какие столбцы будут нужны - предоставьте это Nette. -[Пример, включая схему, можно найти на GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Структура базы данных, используемая в примерах .<> +Фильтрация и сортировка .[#toc-filtering-and-sorting] +===================================================== -Следующий код перечисляет имя автора для каждой книги и все её теги. Мы [обсудим ниже |#Working-with-Relationships], как это работает внутри. +Класс `Selection` предоставляет методы для фильтрации и сортировки данных. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Добавляет условие WHERE. Несколько условий объединяются с помощью AND | +| `whereOr(array $conditions)` | Добавляет группу условий WHERE, объединенных с помощью OR | +| `wherePrimary($value)` | Добавляет условие ГДЕ на основе первичного ключа | +| `order($columns, ...$params)` | Устанавливает сортировку с помощью ORDER BY | +| `select($columns, ...$params)` | Указывает, какие столбцы следует извлечь | +| `limit($limit, $offset = null)` | Ограничивает количество строк (LIMIT) и опционально устанавливает OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Устанавливает пагинацию | +| `group($columns, ...$params)` | Группирует строки (GROUP BY) | +| `having($condition, ...$params)`| Добавляет условие HAVING для фильтрации сгруппированных строк | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author — строка из таблицы 'author' +Методы могут быть объединены в цепочку (так называемый [fluent-интерфейс |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 — строка из таблицы 'tag' - } -} -``` +Эти методы также позволяют использовать специальные обозначения для доступа к [данным из связанных таблиц |#Dotazování přes související tabulky]. -Вы будете довольны тем, насколько эффективно работает слой базы данных. Приведенный выше пример делает постоянное количество запросов, которые выглядят следующим образом: -```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)) -``` +Эскейпинг и идентификаторы .[#toc-escaping-and-identifiers] +----------------------------------------------------------- -Если вы используете [кэш |caching:] (по умолчанию включено), никакие столбцы не будут запрашиваться без необходимости. После первого запроса в кэше будут сохранены имена использованных столбцов, и Nette Database Explorer будет выполнять запросы только с нужными столбцами: +Методы автоматически экранируют параметры и заключают в кавычки идентификаторы (имена таблиц и столбцов), предотвращая SQL-инъекции. Чтобы обеспечить правильную работу, необходимо соблюдать несколько правил: -```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)) +- Записывайте ключевые слова, имена функций, процедур и т. д. в **верхнем регистре**. +- Имена столбцов и таблиц пишите в **строчном регистре**. +- Всегда передавайте строки с помощью **параметров**. + +```php +where('name = ' . $name); // **DISASTER**: уязвимость к SQL-инъекциям +where('name LIKE "%search%"'); // **WRONG**: усложняет автоматическое цитирование +where('name LIKE ?', '%search%'); // **КОРРЕКТНО**: значение передается в качестве параметра + +where('name like ?', $name); // **WRONG**: генерирует: `name` `like` ? +where('name LIKE ?', $name); // **CORRECT**: генерирует: `имя` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`name`) = ? ``` -Выборки .[#toc-selections] -========================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Смотрите возможности фильтрации и ограничения строк [api:Nette\Database\Table\Selection]: +Фильтрует результаты с помощью условий WHERE. Его сильной стороной является интеллектуальная обработка различных типов значений и автоматический выбор операторов SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Устанавливаем WHERE, используя AND как клей, если заданы два или более условий -| `$table->whereOr($where)` | Устанавливаем WHERE, используя OR в качестве связки, если заданы два или более условий -| `$table->order($columns)` | Устанавливаем ORDER BY, например, с помощью выражения `('column DESC, id DESC')`. -| `$table->select($columns)` | Устанавливаем извлеченные столбцы, например, с помощью выражения `('col, MD5(col) AS hash')`. -| `$table->limit($limit[, $offset])` | Устанавливаем LIMIT и OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Включаем пагинацию -| `$table->group($columns)` | Устанавливаем GROUP BY -| `$table->having($having)` | Устанавливаем HAVING +Базовое использование: -Мы можем использовать так называемый [флюентный интерфейс |nette:introduction-to-object-oriented-programming#fluent-interfaces], например `$table->where(...)->order(...)->limit(...)`. Несколько условий `where` или `whereOr` связываются оператором `AND`. +```php +$table->where('id', $value); // ГДЕ `id` = 123 +$table->where('id > ?', $value); // ГДЕ `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' +``` +Благодаря автоматическому определению подходящих операторов вам не нужно разбираться с особыми случаями - Nette сделает это за вас: -where() -------- +```php +$table->where('id', 1); // ГДЕ `id` = 1 +$table->where('id', null); // WHERE `id` IS NULL +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +// Заполнитель ? может использоваться без оператора: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer может автоматически добавлять необходимые операторы для переданных значений: +Метод также корректно обрабатывает отрицательные условия и пустые массивы: -.[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 -- не находит ничего +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- находит все +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- находит все +// $table->where('NOT id ?', $ids); // ВНИМАНИЕ: Этот синтаксис не поддерживается +``` -Вы можете указать заполнитель даже без оператора column. Эти вызовы одинаковы. +Вы также можете передать результат другого запроса к таблице в качестве параметра, создав подзапрос: ```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')); ``` -Эта функция позволяет генерировать правильный оператор на основе значения: +Условия также можно передать в виде массива, объединив элементы с помощью AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// WHERE (`цена_окончательная` < `цена_оригинальная`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Selection корректно обрабатывает и отрицательные условия, работает и для пустых массивов: +В массиве можно использовать пары ключ-значение, и Nette снова автоматически выберет нужные операторы: ```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], +]); +``` + +Мы также можем смешивать SQL-выражения с заполнителями и несколькими параметрами. Это полезно для сложных условий с точно определенными операторами: -// это приведет к исключению, данный синтаксис не поддерживается -$table->where('NOT id ?', $ids); +```php +// WHERE (`возраст` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // два параметра передаются в виде массива +]); ``` +Несколько вызовов `where()` автоматически объединяют условия с помощью AND. -whereOr() ---------- -Пример использования без параметров: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Аналогично `where()`, но объединяет условия с помощью 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, ]); ``` -Мы используем параметры. Если вы не укажете оператор, Nette Database Explorer автоматически добавит соответствующий оператор: +Можно использовать и более сложные выражения: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// WHERE (`цена` > 1000) OR (`цена_с_таксом` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -Ключ может содержать выражение, содержащее подстановочные вопросительные знаки, а затем передавать параметры в значении: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Добавляет условие для первичного ключа таблицы: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// ГДЕ `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Если таблица имеет составной первичный ключ (например, `foo_id`, `bar_id`), мы передаем его в виде массива: + +```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] +-------------------------------------------------------- -Примеры использования: +Указывает порядок, в котором возвращаются строки. Вы можете сортировать по одному или нескольким столбцам, по возрастанию или убыванию, или по пользовательскому выражению: ```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] +--------------------------------------------------------- -Примеры использования: +Указывает столбцы, которые будут возвращены из базы данных. По умолчанию Nette Database Explorer возвращает только те столбцы, которые действительно используются в коде. Используйте метод `select()`, если вам нужно получить конкретные выражения: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Псевдонимы, определенные с помощью `AS`, становятся доступны как свойства объекта `ActiveRow`: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // доступ к псевдониму +} +``` -limit() -------- -Примеры использования: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Ограничивает количество возвращаемых строк (LIMIT) и опционально задает смещение: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (возвращает первые 10 строк) +$table->limit(10, 20); // ОГРАНИЧЕНИЕ 10 СМЕЩЕНИЕ 20 ``` +Для пагинации целесообразнее использовать метод `page()`. + -page() ------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Альтернативный способ установки предела (limit) и смещения (offset): +Упрощает пагинацию результатов. Принимает номер страницы (начиная с 1) и количество элементов на странице. В качестве опции можно передать ссылку на переменную, в которой будет храниться общее количество страниц: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Получение номера последней страницы, переданного в переменную `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Группирует строки по указанным столбцам (GROUP BY). Обычно используется в сочетании с агрегатными функциями: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Подсчитывает количество товаров в каждой категории +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() -------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Примеры использования: +Задает условие для фильтрации сгруппированных строк (HAVING). Может использоваться в сочетании с методом `group()` и агрегатными функциями: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Находит категории с более чем 100 товарами +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() --------- +Чтение данных +============= -Примеры использования: +Для чтения данных из базы данных существует несколько полезных методов: + +.[language-php] +| `foreach ($table as $key => $row)` | Итерация по всем строкам, `$key` - значение первичного ключа, `$row` - объект ActiveRow | +| `$row = $table->get($key)` | Возвращает одну строку по первичному ключу | +| `$row = $table->fetch()` | Возвращает текущую строку и переводит указатель на следующую | +| `$array = $table->fetchPairs()` | Создает ассоциативный массив из результатов | +| `$array = $table->fetchAll()` | Возвращает все строки в виде массива | +| `count($table)` | Возвращает количество строк в объекте Selection | + +Объект [ActiveRow |api:Nette\Database\Table\ActiveRow] доступен только для чтения. Это означает, что вы не можете изменять значения его свойств. Это ограничение обеспечивает согласованность данных и предотвращает неожиданные побочные эффекты. Данные берутся из базы данных, и любые изменения должны производиться явно и контролируемым образом. + + +`foreach` - Итерация по всем строкам +------------------------------------ + +Самый простой способ выполнить запрос и получить строки - это итерация с помощью цикла `foreach`. Он автоматически выполняет SQL-запрос. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = первичный ключ, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Фильтрация по другому значению таблицы .[#toc-joining-key] ----------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Выполняет SQL-запрос и возвращает строку по первичному ключу или `null`, если он не существует. + +```php +$book = $explorer->table('book')->get(123); // возвращает ActiveRow с идентификатором 123 или null +if ($book) { + echo $book->title; +} +``` + -Довольно часто требуется отфильтровать результаты по какому-либо условию, которое включает другую таблицу базы данных. Для таких условий требуются табличные соединения. Однако вам больше не нужно их писать. +fetch(): ?ActiveRow .[method] +----------------------------- -Допустим, вам нужно получить все книги, имя автора которых 'Jon'. Всё, что вам нужно написать, это соединяющий ключ отношения и имя столбца в объединенной таблице. Ключ объединения берется из столбца, который ссылается на таблицу, к которой вы хотите присоединиться. В нашем примере (см. схему db) это столбец `author_id`, и достаточно использовать только его первую часть — `author` (суффикс `_id` можно опустить). `name` — это столбец в таблице `author`, который мы хотим использовать. Условие для переводчика книги (которое связано с колонкой `translator_id`) может быть создано так же просто. +Возвращает одну строку и переводит внутренний указатель на следующую. Если строк больше нет, возвращается `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); +} ``` -Логика соединительных ключей определяется реализацией [Conventions |api:Nette\Database\Conventions]. Мы рекомендуем использовать [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], который анализирует ваши внешние ключи и позволяет легко работать с этими отношениями. -Отношения между книгой и её автором — 1:N. Обратные отношения также возможны. Мы называем это **обратным соединением**. Взгляните на другой пример. Мы хотим получить всех авторов, которые написали более 3 книг. Чтобы сделать соединение обратным, мы используем `:` (двоеточие). Двоеточие означает, что объединенное отношение имеет значение hasMany (и это вполне логично, так как две точки больше, чем одна). К сожалению, класс Selection недостаточно умен, поэтому мы должны помочь с агрегацией и предоставить оператор `GROUP BY`, также условие должно быть записано в виде оператора `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Возвращает результаты в виде ассоциативного массива. В первом аргументе указывается имя столбца, которое будет использоваться в качестве ключа массива, а во втором - имя столбца, которое будет использоваться в качестве значения: ```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', ...] ``` -Вы, наверное, заметили, что выражение объединения относится к книге, но неясно, объединяем ли мы через `author_id` или `translator_id`. В приведенном выше примере Selection соединяется через столбец `author_id`, потому что найдено совпадение с исходной таблицей — таблицей `author`. Если бы такого совпадения не было, и было бы больше возможностей, Nette выбросил бы [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Если указан только ключевой столбец, то значением будет вся строка, т.е. объект `ActiveRow`: -Чтобы выполнить объединение через колонку `translator_id`, предоставьте необязательный параметр в выражении объединения. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +Если в качестве ключа указан `null`, то массив будет иметь числовой индекс, начиная с нуля: ```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', ...] ``` -Давайте рассмотрим более сложное выражение присоединения. +В качестве параметра можно также передать обратный вызов, который вернет либо само значение, либо пару ключ-значение для каждого ряда. Если обратный вызов возвращает только значение, то ключом будет первичный ключ строки: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Первая книга (Ян Новак)', ...]. + +// Обратный вызов также может возвращать массив с парой "ключ и значение": +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Первая книга' => 'Ян Новак', ...]. +``` -Мы хотим найти всех авторов, которые написали что-то о PHP. У всех книг есть теги, поэтому мы должны выбрать тех авторов, которые написали любую книгу с тегом PHP. + +fetchAll(): array .[method] +--------------------------- + +Возвращает все строки в виде ассоциативного массива объектов `ActiveRow`, где ключами являются значения первичного ключа. ```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, ...), ...] ``` -Агрегированные запросы .[#toc-aggregate-queries] ------------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Получаем количество строк -| `$table->count("DISTINCT $column")` | Получаем количество отдельных значений -| `$table->min($column)` | Получаем минимальное значение -| `$table->max($column)` | Получаем максимальное значение -| `$table->sum($column)` | Получаем сумму всех значений -| `$table->aggregation("GROUP_CONCAT($column)")` | Запускаем любую функцию агрегации +Метод `count()` без параметров возвращает количество строк в объекте `Selection`: -.[caution] -Метод `count()` без указания параметров выбирает все записи и возвращает размер массива, что очень неэффективно. Например, если вам нужно подсчитать количество строк для пейджинга, всегда указывайте первый аргумент. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // альтернатива +``` +Примечание: `count()` с параметром выполняет функцию агрегирования COUNT в базе данных, как описано ниже. -Экранирование и кавычки .[#toc-escaping-quoting] -================================================ -Database Explorer умен и избавится от параметров и идентификаторов кавычек за вас. Тем не менее, необходимо соблюдать следующие основные правила: +ActiveRow::toArray(): array .[method] +------------------------------------- -- ключевые слова, функции, процедуры должны быть в верхнем регистре -- столбцы и таблицы должны быть в нижнем регистре -- передавайте переменные в качестве параметров, не объединяйте их +Преобразует объект `ActiveRow` в ассоциативный массив, ключами которого являются имена столбцов, а значениями - соответствующие данные. ```php -->where('name like ?', 'John'); // НЕПРАВИЛЬНО! Генерирует: `name` `like` ? -->where('name LIKE ?', 'John'); // ПРАВИЛЬНО +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray будет состоять из ['id' => 1, 'title' => '...', 'author_id' => ..., ...]. +``` + -->where('KEY = ?', $value); // НЕПРАВИЛЬНО! КЛЮЧ - это ключевое слово -->where('key = ?', $value); // ПРАВИЛЬНО. Генерирует: `key` = ? +Агрегация .[#toc-aggregation] +============================= -->where('name = ' . $name); // Неправильно! sql-инъекция! -->where('name = ?', $name); // ПРАВИЛЬНО +Класс `Selection` предоставляет методы для удобного выполнения функций агрегирования (COUNT, SUM, MIN, MAX, AVG и т. д.). -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // НЕПРАВИЛЬНО! Передавайте переменные как параметры, не конкатенируйте -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // ПРАВИЛЬНО +.[language-php] +| `count($expr)` | Подсчитывает количество строк | +| `min($expr)` | Возвращает минимальное значение в столбце | +| `max($expr)` | Возвращает максимальное значение в столбце | +| `sum($expr)` | Возвращает сумму значений в столбце | +| `aggregation($function)` | Позволяет использовать любую функцию агрегирования, например `AVG()` или `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Выполняет SQL-запрос с функцией COUNT и возвращает результат. Этот метод используется для определения количества строк, соответствующих определенному условию: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -.[warning] -Неправильное использование может привести к образованию дыр в безопасности +Примечание: функция [count() |#count()] без параметра просто возвращает количество строк в объекте `Selection`. + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- -Получение данных .[#toc-fetching-data] -====================================== +Методы `min()` и `max()` возвращают минимальное и максимальное значения в указанном столбце или выражении: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Возвращает сумму значений в указанном столбце или выражении: + +```php +// SELECT SUM(`цена` * `позиции_на_складе`) FROM `продукты` WHERE `активный` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` -| `foreach ($table as $id => $row)` | Итерация по всем строкам результата -| `$row = $table->get($id)` | Получаем одну строку с идентификатором $id из таблицы -| `$row = $table->fetch()` | Получаем следующую строку из результата -| `$array = $table->fetchPairs($key, $value)` | Выборка всех значений в виде ассоциативного массива -| `$array = $table->fetchPairs($value)` | Выборка всех строк в виде ассоциативного массива -| `count($table)` | Получаем количество строк в результирующем наборе + +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Позволяет выполнить любую агрегатную функцию. + +```php +// Вычисляет среднюю цену товаров в категории +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Объединяет теги товаров в одну строку +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Если нам нужно агрегировать результаты, которые сами являются результатом агрегирования и группировки (например, `SUM(value)` над сгруппированными строками), то в качестве второго аргумента мы указываем функцию агрегирования, которая будет применяться к этим промежуточным результатам: + +```php +// Рассчитывает общую цену товаров на складе для каждой категории, затем суммирует эти цены +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +В этом примере мы сначала вычисляем общую цену товаров в каждой категории (`SUM(price * stock) AS category_total`) и группируем результаты по `category_id`. Затем мы используем `aggregation('SUM(category_total)', 'SUM')` для суммирования этих промежуточных итогов. Второй аргумент `'SUM'` задает функцию агрегирования, которую нужно применить к промежуточным результатам. Вставка, обновление и удаление .[#toc-insert-update-delete] =========================================================== -Метод `insert()` принимает массив объектов Traversable (например, [ArrayHash |utils:arrays#ArrayHash], который возвращает [forms|forms:]): +Nette Database Explorer упрощает вставку, обновление и удаление данных. Все перечисленные методы выбрасывают сообщение `Nette\Database\DriverException` в случае ошибки. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Вставляет новые записи в таблицу. + +**Вставка одной записи:**. + +Новая запись передается в виде ассоциативного массива или итерируемого объекта (например, `ArrayHash`, используемого в [формах |forms:]), где ключи соответствуют именам столбцов в таблице. + +Если таблица имеет определенный первичный ключ, метод возвращает объект `ActiveRow`, который перезагружается из базы данных, чтобы отразить любые изменения, сделанные на уровне базы данных (например, триггеры, значения столбцов по умолчанию или вычисления с автоинкрементами). Это обеспечивает согласованность данных, и объект всегда содержит текущие данные базы данных. Если первичный ключ не определен явно, метод возвращает входные данные в виде массива. ```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 - это экземпляр ActiveRow, содержащий полные данные вставленного ряда, +// включая автоматически сгенерированный идентификатор и любые изменения, сделанные триггерами +echo $row->id; // Выводит идентификатор нового вставленного пользователя +echo $row->created_at; // Выводит время создания, если оно установлено триггером ``` -Если для таблицы определен первичный ключ, возвращается объект ActiveRow, содержащий вставленную строку. +**Вставка нескольких записей одновременно:**. -Вставка нескольких значений: +Метод `insert()` позволяет вставить несколько записей с помощью одного SQL-запроса. В этом случае он возвращает количество вставленных строк. ```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` (`имя`, `год`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows будет равно 2 ``` -В качестве параметров можно передавать файлы или объекты DateTime: +В качестве параметра можно также передать объект `Selection` с выборкой данных. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Вставка специальных значений:** + +Значения могут включать файлы, объекты `DateTime` или литералы SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // или $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // вставляет файл + 'name' => 'John', + 'created_at' => new DateTime, // преобразование в формат базы данных + 'avatar' => fopen('image.jpg', 'rb'), // вставляет содержимое двоичного файла + 'uuid' => $explorer::literal('UUID()'), // вызывает функцию UUID() ]); ``` -Обновление (возвращает количество затронутых строк): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Обновляет строки в таблице на основе заданного фильтра. Возвращает количество фактически измененных строк. + +Обновляемые столбцы передаются в виде ассоциативного массива или итерируемого объекта (например, `ArrayHash`, используемого в [формах |forms:]), где ключи соответствуют именам столбцов в таблице: ```php -$count = $explorer->table('users') - ->where('id', 10) // должен вызываться до 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 ``` -Для обновления мы можем использовать операторы `+=` и `-=`: +Для изменения числовых значений можно использовать операторы `+=` и `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // увеличивает значение столбца "очки" на 1 + 'coins-=' => 1, // уменьшает значение столбца 'coins' на 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Удаление (возвращает количество удаленных строк): + +Selection::delete(): int .[method] +---------------------------------- + +Удаляет строки из таблицы на основе заданного фильтра. Возвращает количество удаленных строк. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +При вызове `update()` или `delete()` обязательно используйте `where()` для указания обновляемых или удаляемых строк. Если `where()` не используется, операция будет выполнена над всей таблицей! + -Работа с отношениями .[#toc-working-with-relationships] -======================================================= +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Обновляет данные в строке базы данных, представленной объектом `ActiveRow`. В качестве параметра он принимает итерируемые данные, где ключами являются имена столбцов. Для изменения числовых значений можно использовать операторы `+=` и `-=`: -Один к одному ("has one") .[#toc-has-one-relation] --------------------------------------------------- -Отношение «Один к одному» — распространенный случай использования. У книги *есть один* автор. Книга *имеет одного* переводчика. Получение связанной строки в основном осуществляется методом `ref()`. Он принимает два аргумента: имя целевой таблицы и столбец исходного соединения. См. пример: +После выполнения обновления `ActiveRow` автоматически перезагружается из базы данных, чтобы отразить все изменения, сделанные на уровне базы данных (например, триггерами). Метод возвращает `true` только в том случае, если произошло реальное изменение данных. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // увеличивает количество просмотров +]); +echo $article->views; // Выводит текущее количество просмотров ``` -В приведенном выше примере мы извлекаем связанную запись об авторе из таблицы `author`, поиск первичного ключа автора осуществляется по столбцу `book.author_id`. Метод Ref() возвращает экземпляр ActiveRow или null, если нет подходящей записи. Возвращенная строка является экземпляром ActiveRow, поэтому мы можем работать с ней так же, как и с записью книги. +Этот метод обновляет только одну конкретную строку в базе данных. Для массового обновления нескольких строк используйте метод [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Удаляет из базы данных строку, представленную объектом `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Удаление книги с идентификатором 1 +``` -// или напрямую -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Этот метод удаляет только один конкретный ряд в базе данных. Для массового удаления нескольких строк используйте метод [Selection::delete() |#Selection::delete()]. + + +Отношения между таблицами .[#toc-relationships-between-tables] +============================================================== + +В реляционных базах данных данные разделены на несколько таблиц и связаны между собой с помощью внешних ключей. Nette Database Explorer предлагает революционный способ работы с этими отношениями - без написания запросов JOIN и без необходимости конфигурирования или генерации сущностей. + +Для демонстрации мы воспользуемся базой данных **example**[(доступна на GitHub |https://github.com/nette-examples/books]). База данных включает в себя следующие таблицы: + +- `author` - авторы и переводчики (столбцы `id`, `name`, `web`, `born`) +- `book` - книги (столбцы `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - теги (колонки `id`, `name`) +- `book_tag` - таблица связей между книгами и тегами (колонки `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Структура базы данных .<> + +В этом примере базы данных книг мы видим несколько типов связей (упрощенных по сравнению с реальностью): + +- **Один-ко-многим (1:N)** - У каждой книги **есть один** автор; автор может написать **множество** книг. +- **Зеро-ко-многим (0:N)** - У книги **может быть** переводчик; переводчик может перевести **множество** книг. +- **Zero-to-one (0:1)** - Книга **может иметь** продолжение. +- **Много-ко-многим (M:N)** - Книга **может иметь несколько** тегов, и один тег может быть присвоен **нескольким** книгам. + +В этих отношениях всегда есть **родительская таблица** и **детская таблица**. Например, в отношениях между авторами и книгами таблица `author` является родительской, а таблица `book` - дочерней - можно считать, что книга всегда "принадлежит" одному автору. Это также отражено в структуре базы данных: дочерняя таблица `book` содержит внешний ключ `author_id`, который ссылается на родительскую таблицу `author`. + +Если мы хотим отобразить книги вместе с именами их авторов, у нас есть два варианта. Либо мы получаем данные с помощью одного SQL-запроса с JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Либо мы получаем данные в два этапа - сначала книги, затем их авторов - и собираем их в PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -У книги также один переводчик, поэтому узнать имя переводчика довольно просто. +Второй подход, как ни странно, **более эффективен**. Данные извлекаются только один раз и могут быть лучше использованы в кэше. Именно так работает Nette Database Explorer - он обрабатывает все под капотом и предоставляет вам чистый 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 - это запись из таблицы 'author'. + echo 'translated by: ' . $book->translator?->name; +} ``` -Всё это хорошо, но несколько громоздко, не находите? Database Explorer уже содержит определения внешних ключей, так почему бы не использовать их автоматически? Давайте сделаем это! -Если мы вызываем свойство, которого не существует, ActiveRow пытается разрешить имя вызывающего свойства как отношение 'has one'. Получение этого свойства аналогично вызову метода ref() только с одним аргументом. Мы будем называть единственный аргумент **key**. Ключ будет разрешен в конкретное отношение внешнего ключа. Переданный ключ сопоставляется со столбцами строки, и если он совпадает, то внешний ключ, определенный в сопоставленном столбце, используется для получения данных из связанной целевой таблицы. См. пример: +Доступ к родительской таблице .[#toc-accessing-the-parent-table] +---------------------------------------------------------------- + +Доступ к родительской таблице очень прост. Это такие отношения, как *у книги есть автор* или *у книги может быть переводчик*. Доступ к связанной записи можно получить через свойство объекта `ActiveRow` - имя свойства совпадает с именем столбца внешнего ключа без суффикса `id`: ```php -$book->author->name; -// то же самое -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // находит автора по столбцу 'author_id' +echo $book->translator?->name; // находит переводчика по столбцу 'translator_id' ``` -Экземпляр ActiveRow не имеет колонки автора. Все столбцы книги ищутся на предмет совпадения с *key*. Совпадение в данном случае означает, что имя столбца должно содержать ключ. Так, в приведенном примере столбец `author_id` содержит строку 'author' и поэтому сопоставляется с ключом 'author'. Если вы хотите получить переводчика книги, то в качестве ключа можно использовать, например, 'translator', так как ключ 'translator' будет соответствовать столбцу `translator_id`. Подробнее о логике подбора ключей вы можете прочитать в главе [Joining expressions |#joining-key]. +При обращении к свойству `$book->author` Explorer ищет в таблице `book` столбец, содержащий строку `author` (например, `author_id`). На основании значения в этом столбце он извлекает соответствующую запись из таблицы `author` и возвращает ее в виде объекта `ActiveRow`. Аналогично, `$book->translator` использует столбец `translator_id`. Поскольку столбец `translator_id` может содержать `null`, используется оператор `?->`. + +Альтернативный подход обеспечивается методом `ref()`, который принимает два аргумента - имя целевой таблицы и связывающий столбец - и возвращает экземпляр `ActiveRow` или `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; // ссылка на автора +echo $book->ref('author', 'translator_id')->name; // ссылка на переводчика ``` -Если вы хотите получить несколько книг, используйте тот же подход. Nette Database Explorer найдет авторов и переводчиков сразу для всех найденных книг. +Метод `ref()` полезен, если доступ на основе свойств не может быть использован, например, когда таблица содержит столбец с тем же именем, что и свойство (`author`). В других случаях рекомендуется использовать доступ на основе свойств для лучшей читабельности. + +Explorer автоматически оптимизирует запросы к базе данных. При итерации книг и доступе к связанным с ними записям (авторы, переводчики) Explorer не генерирует запрос для каждой книги в отдельности. Вместо этого он выполняет только **один запрос SELECT для каждого типа отношений**, что значительно снижает нагрузку на базу данных. Например: ```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; } ``` -Код будет выполнять только эти 3 запроса: +Этот код выполнит только три оптимизированных запроса к базе данных: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Логика определения связующего столбца определяется реализацией [Conventions |api:Nette\Database\Conventions]. Мы рекомендуем использовать [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], которая анализирует внешние ключи и позволяет беспрепятственно работать с существующими связями таблиц. -Один ко многим ("has many") .[#toc-has-many-relation] ------------------------------------------------------ -Отношение «один ко многим» — это просто обратное отношение «один к одному». Автор *написал* *много* книг. Автор *перевел* *много* книг. Как видите, этот тип отношения немного сложнее, потому что отношение является «именованным» ("написал", "перевел"). У экземпляра ActiveRow есть метод `related()`, который возвращает массив связанных записей. Записи также являются экземплярами ActiveRow. См. пример ниже: +Доступ к дочерней таблице .[#toc-accessing-the-child-table] +----------------------------------------------------------- + +Доступ к дочерней таблице работает в обратном направлении. Теперь мы спрашиваем *какие книги написал этот автор* или *какие книги перевел этот переводчик*. Для этого типа запроса мы используем метод `related()`, который возвращает объект `Selection` с соответствующими записями. Вот пример: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' написал:'; +$author = $explorer->table('author')->get(1); +// Выводит все книги, написанные автором foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'и перевёл:'; +// Выводит все книги, переведенные автором foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Метод `related()` принимает полное описание соединения, передаваемое как два аргумента или как один аргумент, соединённый точкой. Первый аргумент — целевая таблица, второй — целевой столбец. +Метод `related()` принимает описание отношения как один аргумент с использованием точечной нотации или как два отдельных аргумента: ```php -$author->related('book.translator_id'); -// то же самое -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // один аргумент +$author->related('book', 'translator_id'); // два аргумента ``` -Вы можете использовать эвристику Nette Database Explorer, основанную на внешних ключах, и указать только аргумент **key**. Ключ будет сопоставлен со всеми внешними ключами, указывающими на текущую таблицу (таблица `author`). Если есть совпадение, Nette Database Explorer будет использовать этот внешний ключ, в противном случае он выбросит [Nette\InvalidArgumentException|api:Nette\InvalidArgumentException] или [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Подробнее о логике подбора ключей вы можете прочитать в главе [Joining expressions |#joining-key]. +Explorer может автоматически определить правильный столбец связи на основе имени родительской таблицы. В данном случае связь устанавливается через столбец `book.author_id`, поскольку имя исходной таблицы - `author`: -Конечно, вы можете вызвать связанные методы для всех найденных авторов, и Nette Database Explorer снова получит соответствующие книги сразу. +```php +$author->related('book'); // использует book.author_id +``` + +Если существует несколько возможных связей, Explorer выбросит исключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Конечно, мы также можем использовать метод `related()` при циклическом переборе нескольких записей, и Explorer автоматически оптимизирует запросы и в этом случае: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' написал:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -В приведенном выше примере будет выполнено только два запроса: +Этот код генерирует только два эффективных SQL-запроса: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- идентификаторы найденных авторов +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors +``` + + +Отношения "многие-ко-многим .[#toc-many-to-many-relationship] +------------------------------------------------------------- + +Для отношений "многие-ко-многим" (M:N) требуется **таблица-перекресток** (в нашем случае `book_tag`). Эта таблица содержит два столбца с внешними ключами (`book_id`, `tag_id`). Каждый столбец ссылается на первичный ключ одной из связанных таблиц. Чтобы получить связанные данные, мы сначала извлекаем записи из таблицы связей с помощью `related('book_tag')`, а затем переходим к целевым данным: + +```php +$book = $explorer->table('book')->get(1); +// Выводит имена тегов, присвоенных книге +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // получает название тега через таблицу ссылок +} + +$tag = $explorer->table('tag')->get(1); +// Противоположное направление: выводит названия книг с данным тегом +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // получает название книги +} +``` + +Explorer снова оптимизирует SQL-запросы в эффективную форму: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag ``` -Создание Explorer вручную .[#toc-creating-explorer-manually] -============================================================ +Запрос через связанные таблицы .[#toc-querying-through-related-tables] +---------------------------------------------------------------------- -Соединение с базой данных может быть создано с помощью конфигурации приложения. В таких случаях создается служба `Nette\Database\Explorer`, которая может быть передана в качестве зависимости с помощью DI-контейнера. +В методах `where()`, `select()`, `order()` и `group()` можно использовать специальные обозначения для доступа к столбцам из других таблиц. Explorer автоматически создает необходимые JOIN. -Однако, если Nette Database Explorer используется как самостоятельный инструмент, экземпляр объекта `Nette\Database\Explorer` должен быть создан вручную. +**Точечная нотация** (`parent_table.column`) используется для отношений 1:N с точки зрения родительской таблицы: ```php -// $storage implements Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Находит книги, имена авторов которых начинаются с "Jon". +$books->where('author.name LIKE ?', 'Jon%'); + +// Сортирует книги по имени автора по убыванию +$books->order('author.name DESC'); + +// Выводит название книги и имя автора +$books->select('book.title, author.name'); +``` + +**Точечная нотация** используется для отношений 1:N с точки зрения родительской таблицы: + +```php +$authors = $explorer->table('author'); + +// Находит авторов, написавших книги с 'PHP' в названии +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Подсчитывает количество книг для каждого автора +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +В приведенном выше примере с обозначением двоеточия (`:book.title`) столбец внешнего ключа явно не указан. Explorer автоматически определяет нужный столбец на основе имени родительской таблицы. В данном случае соединение выполняется через столбец `book.author_id`, поскольку имя исходной таблицы - `author`. Если существует несколько возможных соединений, Explorer выбрасывает исключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Связывающий столбец можно явно указать в круглых скобках: + +```php +// Находит авторов, которые перевели книгу с 'PHP' в названии +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Нотации можно объединять в цепочки для доступа к данным в нескольких таблицах: + +```php +// Поиск авторов книг, отмеченных тегом 'PHP'. +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Расширение условий для JOIN .[#toc-extending-conditions-for-join] +----------------------------------------------------------------- + +Метод `joinWhere()` добавляет дополнительные условия к объединению таблиц в SQL после ключевого слова `ON`. + +Например, мы хотим найти книги, переведенные определенным переводчиком: + +```php +// Находит книги, переведенные переводчиком по имени 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +В условии `joinWhere()` можно использовать те же конструкции, что и в методе `where()`, - операторы, заполнители, массивы значений или выражения SQL. + +Для более сложных запросов с несколькими JOIN можно определить псевдонимы таблиц: + +```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) +``` + +Обратите внимание, что если метод `where()` добавляет условия в предложение `WHERE`, то метод `joinWhere()` расширяет условия в предложении `ON` при объединении таблиц. + + +Создание проводника вручную .[#toc-manually-creating-explorer] +============================================================== + +Если вы не используете контейнер Nette DI, вы можете создать экземпляр `Nette\Database\Explorer` вручную: + +```php +use Nette\Database; + +// $storage реализует Nette\Caching\Storage, например: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// подключение к базе данных +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// управляет отражением структуры базы данных +$structure = new Database\Structure($connection, $storage); +// определяет правила сопоставления имен таблиц, столбцов и внешних ключей +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/ru/security.texy b/database/ru/security.texy new file mode 100644 index 0000000000..8c31103409 --- /dev/null +++ b/database/ru/security.texy @@ -0,0 +1,145 @@ +Риски безопасности +****************** + +.[perex] +Базы данных часто содержат конфиденциальные данные и позволяют выполнять опасные операции. Nette Database предоставляет ряд функций безопасности. Однако очень важно понимать разницу между безопасными и небезопасными API. + + +SQL-инъекция .[#toc-sql-injection] +================================== + +SQL-инъекция - самый серьезный риск безопасности при работе с базами данных. Она возникает, когда непроверенный пользовательский ввод становится частью SQL-запроса. Злоумышленник может внедрить свои собственные SQL-команды, получить или изменить данные в базе данных. + +```php +// ❌ UNSAFE CODE - уязвимость к SQL-инъекции +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Злоумышленник может ввести что-то вроде: ' OR '1'='1 +// В результате будет получен запрос: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Это вернет всех пользователей! +``` + +То же самое относится и к Database Explorer: + +```php +// ❌ КОД БЕЗОПАСНОСТИ +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Безопасные параметризованные запросы .[#toc-safe-parameterized-queries] +======================================================================= + +Безопасным способом вставки значений в SQL-запросы являются параметризованные запросы. Nette Database предоставляет несколько способов их использования. + + +Вопросительные знаки .[#toc-placeholder-question-marks] +------------------------------------------------------- + +Самый простой метод - использовать вопросительные знаки-пустышки: + +```php +// ✅ Безопасные параметризованные запросы +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Безопасное условие в Проводнике +$table->where('name = ?', $_GET['name']); +``` + +То же самое относится и ко всем другим методам в Database Explorer, позволяющим вставлять выражения с вопросительными знаками и параметрами. + +.[warning] +Значения должны быть скалярного типа (`string`, `int`, `float`, `bool`) или `null`. Если, например, `$_GET['name']` является массивом, Nette Database включит все его элементы в SQL-запрос, что может быть нежелательно. + + +Массивы значений .[#toc-value-arrays] +------------------------------------- + +В пунктах `INSERT`, `UPDATE` или `WHERE` можно использовать массивы значений: + +```php +// ✅ Безопасный INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Безопасный UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Database автоматически экранирует все значения, передаваемые через параметризованные запросы. Однако мы должны обеспечить правильный тип данных параметров. + + +Ключи массивов не являются безопасным API .[#toc-array-keys-are-not-a-safe-api] +=============================================================================== + +В то время как значения в массивах безопасны, то же самое нельзя сказать о ключах: + +```php +// ❌ UNSAFE CODE - ключи могут содержать SQL-инъекцию +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Для команд `INSERT` и `UPDATE` это критический недостаток безопасности - злоумышленник может вставить или изменить любой столбец в базе данных. Например, они могут установить `is_admin = 1` или вставить произвольные данные в чувствительные столбцы. + +В условиях `WHERE` это еще более опасно, поскольку позволяет **SQL enumeration** - технику постепенного получения информации о базе данных. Злоумышленник может попытаться узнать о зарплате сотрудников, внедрив в `$_GET` вот такой код: + +```php +$_GET = ['salary >', 100000]; // начинает определять диапазоны зарплат +``` + +Основная проблема, однако, заключается в том, что условия `WHERE` поддерживают SQL-выражения в ключах: + +```php +// Легитимное использование операторов в ключах +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ UNSAFE: злоумышленник может внедрить свой собственный SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // позволяет злоумышленнику получить зарплату администратора +``` + +Это снова **SQL-инъекция**. + + +Белые списки столбцов .[#toc-whitelisting-columns] +-------------------------------------------------- + +Если вы хотите разрешить пользователям выбирать колонки, всегда используйте белый список: + +```php +// ✅ Безопасная обработка - только разрешенные столбцы +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Динамические идентификаторы .[#toc-dynamic-identifiers] +======================================================= + +Для динамических имен таблиц и столбцов используйте заполнитель `?name`: + +```php +// ✅ Безопасное использование доверенных идентификаторов +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - никогда не используйте пользовательский ввод +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Символ `?name` следует использовать только для доверенных значений, определенных в коде приложения. Для значений, предоставленных пользователем, снова используйте белый список. diff --git a/database/sl/@left-menu.texy b/database/sl/@left-menu.texy index 30c2a20bb2..61738f7f2f 100644 --- a/database/sl/@left-menu.texy +++ b/database/sl/@left-menu.texy @@ -4,3 +4,4 @@ Podatkovna zbirka - [Raziskovalec |Explorer] - [Razmislek |Reflection] - [Konfiguracija |Configuration] +- [Varnostna tveganja |security] diff --git a/database/sl/explorer.texy b/database/sl/explorer.texy index 0c3eb3a793..4f6251f6d7 100644 --- a/database/sl/explorer.texy +++ b/database/sl/explorer.texy @@ -3,548 +3,927 @@ Raziskovalec zbirke podatkov
-Nette Database Explorer bistveno poenostavi pridobivanje podatkov iz podatkovne zbirke brez pisanja poizvedb SQL. +Nette Database Explorer je zmogljiv sloj, ki bistveno poenostavi pridobivanje podatkov iz podatkovne zbirke, ne da bi bilo treba pisati poizvedbe SQL. -- uporablja učinkovite poizvedbe -- podatki se ne prenašajo po nepotrebnem -- ima elegantno sintakso +- Delo s podatki je naravno in enostavno za razumevanje +- Ustvarja optimizirane poizvedbe SQL, ki poberejo le potrebne podatke +- Zagotavlja enostaven dostop do povezanih podatkov, ne da bi bilo treba pisati poizvedbe JOIN +- Deluje takoj, brez kakršne koli konfiguracije ali ustvarjanja entitet
-Če želite uporabiti Raziskovalca podatkovne zbirke, začnite s tabelo - na objektu [api:Nette\Database\Explorer] pokličite `table()`. Najlažji način za pridobitev instance objekta konteksta je [opisan tukaj |core#Connection and Configuration], za primer, ko Nette Database Explorer uporabljate kot samostojno orodje, pa ga lahko [ustvarite ročno |#Creating Explorer Manually]. +Nette Database Explorer je razširitev nizkonivojske plasti [Nette Database Core |core], ki dodaja priročen objektno usmerjen pristop k upravljanju podatkovne zbirke. + +Delo z Raziskovalcem se začne s klicem metode `table()` na objektu [api:Nette\Database\Explorer] (kako ga pridobiti, je [opisano tukaj |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // ime tabele db je 'book' +$books = $explorer->table('book'); // 'book' je ime tabele ``` -Klic vrne primerek objekta [Izbor |api:Nette\Database\Table\Selection], nad katerim lahko iterirate, da pridobite vse knjige. Vsak element (vrstica) je predstavljen z instanco [ActiveRow |api:Nette\Database\Table\ActiveRow] s podatki, ki so preslikani na njegove lastnosti: +Metoda vrne objekt [Selection |api:Nette\Database\Table\Selection], ki predstavlja poizvedbo SQL. Na ta objekt je mogoče verižno povezati dodatne metode za filtriranje in razvrščanje rezultatov. Poizvedba se sestavi in izvede le, ko so podatki zahtevani, na primer z iteracijo s `foreach`. Vsaka vrstica je predstavljena z objektom [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // izpisi stolpca 'naslov' + echo $book->author_id; // izpiše stolpec 'author_id'. } ``` -Za pridobitev samo ene določene vrstice se uporabi metoda `get()`, ki neposredno vrne primerek ActiveRow. +Raziskovalec močno poenostavi delo z [razmerji med tabelami |#Vazby mezi tabulkami]. Naslednji primer prikazuje, kako preprosto lahko izpišemo podatke iz povezanih tabel (knjige in njihovi avtorji). Opazite, da ni treba pisati nobenih poizvedb JOIN; Nette jih ustvari namesto nas: ```php -$book = $explorer->table('book')->get(2); // vrne knjigo z id 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // ustvari povezavo JOIN s tabelo "author". +} ``` -Oglejmo si pogost primer uporabe. Pridobiti morate knjige in njihove avtorje. To je običajno razmerje 1:N. Pogosto uporabljena rešitev je pridobivanje podatkov z eno poizvedbo SQL z združevanjem tabel. Druga možnost je, da podatke pridobite ločeno, zaženete eno poizvedbo za pridobitev knjig in nato z drugo poizvedbo (npr. v ciklu foreach) pridobite avtorja za vsako knjigo. To bi lahko zlahka optimizirali tako, da bi izvedli le dve poizvedbi, eno za knjige in drugo za potrebne avtorje - in točno tako to počne Nette Database Explorer. +Nette Database Explorer optimizira poizvedbe za največjo učinkovitost. Zgornji primer izvede le dve poizvedbi SELECT, ne glede na to, ali obdelujemo 10 ali 10.000 knjig. -V spodnjih primerih bomo delali s shemo podatkovne zbirke na sliki. Obstajajo povezave OneHasMany (1:N) (avtor knjige `author_id` in morebitni prevajalec `translator_id`, ki je lahko `null`) in ManyHasMany (M:N) med knjigo in njenimi oznakami. +Poleg tega Raziskovalec spremlja, kateri stolpci se uporabljajo v kodi, in iz zbirke podatkov pobere le te, s čimer prihrani še dodatno zmogljivost. To obnašanje je popolnoma samodejno in prilagodljivo. Če pozneje spremenite kodo in uporabite dodatne stolpce, Raziskovalec samodejno prilagodi poizvedbe. Ničesar vam ni treba konfigurirati ali razmišljati o tem, kateri stolpci bodo potrebni - to prepustite Nette. -[Primer, vključno s shemo, je na voljo na GitHubu |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Struktura zbirke podatkov, uporabljena v primerih .<> +Filtriranje in razvrščanje .[#toc-filtering-and-sorting] +======================================================== -V naslednji kodi je za vsako knjigo navedeno ime avtorja in vse njegove oznake. [O tem, |#Working with relationships] kako to deluje interno, bomo [razpravljali |#Working with relationships] v naslednjem trenutku. +Razred `Selection` ponuja metode za filtriranje in razvrščanje podatkov. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Doda pogoj WHERE. Več pogojev se združi z uporabo AND | +| `whereOr(array $conditions)` | Doda skupino pogojev WHERE, združenih z uporabo OR | +| `wherePrimary($value)` | Doda pogoj WHERE na podlagi primarnega ključa | +| `order($columns, ...$params)` | Določi razvrščanje z ORDER BY | +| `select($columns, ...$params)` | Določi, katere stolpce je treba poiskati | +| `limit($limit, $offset = null)` | Omeji število vrstic (LIMIT) in po želji določi OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Nastavi paginacijo | +| `group($columns, ...$params)` | Združi vrstice v skupine (GROUP BY) | +| `having($condition, ...$params)`| Doda pogoj HAVING za filtriranje združenih vrstic | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author je vrstica iz tabele 'avtor' +Metode je mogoče verižiti (tako imenovani [tekoči vmesnik |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 vrstica iz tabele 'tag' - } -} -``` +Te metode omogočajo tudi uporabo posebnih zapisov za dostop do [podatkov iz povezanih tabel |#Dotazování přes související tabulky]. -Zadovoljni boste, kako učinkovito deluje plast podatkovne zbirke. Zgornji primer izvaja stalno število zahtevkov, ki so videti takole: -```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)) -``` +Pobegi in identifikatorji .[#toc-escaping-and-identifiers] +---------------------------------------------------------- -Če uporabljate [predpomnilnik |caching:] (privzeto vklopljen), ne boste po nepotrebnem poizvedovali po nobenem stolpcu. Po prvi poizvedbi bo predpomnilnik shranil uporabljena imena stolpcev in Nette Database Explorer bo izvajal poizvedbe samo s potrebnimi stolpci: +Metode samodejno izločijo parametre in identifikatorje citatov (imena tabel in stolpcev), kar preprečuje vbrizgavanje SQL. Za zagotovitev pravilnega delovanja je treba upoštevati nekaj pravil: -```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)) +- ključne besede, imena funkcij, postopkov itd. pišite z **nadrobnimi črkami**. +- Imena stolpcev in tabel pišite z malimi črkami**. +- Nizove vedno prenašajte z uporabo **parametrov**. + +```php +where('name = ' . $name); // **DISASTER**: ranljiv za vbrizgavanje SQL +where('name LIKE "%search%"'); // **PRAVILNO**: otežuje samodejno citiranje +where('name LIKE ?', '%search%'); // **CORRECT**: vrednost je posredovana kot parameter + +where('name like ?', $name); // **WRONG**: ustvari: ? +where('name LIKE ?', $name); // **CORRECT**: generira: `name` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`name`) = ? ``` -Izbori .[#toc-selections] -========================= +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Oglejte si možnosti za filtriranje in omejevanje vrstic [api:Nette\Database\Table\Selection]: +Filtriranje rezultatov z uporabo pogojev WHERE. Njegova prednost je v inteligentnem obravnavanju različnih vrst vrednosti in samodejnem izbiranju operatorjev SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Nastavite WHERE z uporabo AND kot lepilo, če sta podana dva ali več pogojev -| `$table->whereOr($where)` | Nastavitev WHERE z uporabo OR kot veziva, če sta podana dva ali več pogojev -| `$table->order($columns)` | Nastavitev ORDER BY, lahko je izraz `('column DESC, id DESC')` -| `$table->select($columns)` | Nastavitev pridobljenih stolpcev, lahko izraz `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | Nastavite LIMIT in OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Omogoča paginiranje -| `$table->group($columns)` | Nastavitev GROUP BY -| `$table->having($having)` | Nastavitev HAVING +Osnovna uporaba: -Uporabimo lahko tako imenovani [tekoči vmesnik |nette:introduction-to-object-oriented-programming#fluent-interfaces], na primer `$table->where(...)->order(...)->limit(...)`. Več pogojev `where` ali `whereOr` povežemo z operatorjem `AND`. +```php +$table->where('id', $value); // Kjer `id` = 123 +$table->where('id > ?', $value); // Kjer `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // KJE `id` = 1 ALI `imeno` = 'Jon Snow' +``` +Zahvaljujoč samodejnemu zaznavanju ustreznih operatorjev vam ni treba obravnavati posebnih primerov - Nette jih uredi namesto vas: -kjer() .[#toc-where] --------------------- +```php +$table->where('id', 1); // Kjer `id` = 1 +$table->where('id', null); // Kjer je `id` NULL +$table->where('id', [1, 2, 3]); // Kjer `id` IN (1, 2, 3) +// Namestni znak ? se lahko uporablja brez operatorja: +$table->where('id ?', 1); // Kjer `id` = 1 +``` -Nette Database Explorer lahko samodejno doda potrebne operatorje za posredovane vrednosti: +Metoda pravilno obravnava tudi negativne pogoje in prazne pole: -.[language-php] -| `$table->where('field', $value)` | polje = $vrednost -| `$table->where('field', null)` | polje JE NULL -| `$table->where('field > ?', $val)` | polje > $val -| `$table->where('field', [1, 2])` | field IN (1, 2) -| `$table->where('id = ? OR name = ?', 1, $name)` | id = 1 ALI ime = 'Jon Snow' -| `$table->where('field', $explorer->table($tableName))` | field IN (SELECT $primary FROM $tableName) -| `$table->where('field', $explorer->table($tableName)->select('col'))` | polje IN (SELECT col FROM $tableName) +```php +$table->where('id', []); // Kjer `id` JE NULL IN FALSE -- ne najde ničesar +$table->where('id NOT', []); // KER je `id` NIČ ALI PRAVDA -- najde vse +$table->where('NOT (id ?)', []); // KER NE (`id` JE NULL IN FALSE) -- najde vse +// $table->where('NOT id ?', $ids); // OPOZORILO: Ta sintaksa ni podprta +``` -Namestno oznako lahko zagotovite tudi brez operatorja stolpca. Ti klici so enaki. +Kot parameter lahko posredujete tudi rezultat druge poizvedbe po tabeli in tako ustvarite podpovpraševanje: ```php -$table->where('id = ? OR id = ?', 1, 2); -$table->where('id ? OR id ?', 1, 2); +// Kjer `id` IN (SELECT `id` FROM `tableName`) +$table->where('id', $explorer->table($tableName)); + +// Kjer `id` IN (SELECT `col` FROM `tableName`) +$table->where('id', $explorer->table($tableName)->select('col')); ``` -Ta funkcija omogoča ustvarjanje pravilnega operatorja na podlagi vrednosti: +Pogoje lahko posredujete tudi kot polje, pri čemer elemente združite z uporabo metode AND: ```php -$table->where('id ?', 2); // id = 2 -$table->where('id ?', null); // id IS NULL -$table->where('id', $ids); // id IN (...) +// Kjer (`Cena_končna` < `Cena_prejšnja`) IN (Število zalog` > `Min_zaloge`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); ``` -Izbira pravilno obravnava tudi negativne pogoje, deluje tudi pri praznih poljih: +V polju se lahko uporabijo pari ključ-vrednost, Nette pa bo ponovno samodejno izbral pravilne operatorje: ```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) +// Kjer (`status` = 'active') IN (`id` V (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` -// to vrže izjemo, ker ta sintaksa ni podprta -$table->where('NOT id ?', $ids); +Prav tako lahko mešamo izraze SQL z nadomestnimi znaki in več parametri. To je uporabno za zapletene pogoje z natančno določenimi operatorji: + +```php +// Kjer (`starost` > 18 let) IN (ROUND(`score`, 2) > 75,5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // dva parametra sta posredovana kot polje +]); ``` +Več klicev na `where()` samodejno združi pogoje z uporabo AND. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Primer uporabe brez parametrov: +Podobno kot `where()`, vendar združuje pogoje z uporabo OR: ```php -// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`)) +// Kjer (`status` = 'aktiven') ALI (`izbrisano` = 1) $table->whereOr([ - 'user_id IS NULL', - 'SUM(field1) > SUM(field2)', + 'status' => 'active', + 'deleted' => true, ]); ``` -Uporabljamo parametre. Če ne določite operaterja, bo Nette Database Explorer samodejno dodal ustreznega: +Uporabljajo se lahko tudi bolj zapleteni izrazi: ```php -// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11) +// Kjer (`cena` > 1000) ALI (`cena_z_davkom` > 1500) $table->whereOr([ - 'field1' => null, - 'field2' => [3, 5], - 'amount >' => 11, + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, ]); ``` -Ključ lahko vsebuje izraz, ki vsebuje nadomestne vprašalnike, nato pa v vrednosti posredujemo parametre: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Doda pogoj za primarni ključ tabele: + +```php +// Kjer `id` = 123 +$table->wherePrimary(123); + +// Kjer `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Če ima tabela sestavljen primarni ključ (npr. `foo_id`, `bar_id`), ga posredujemo kot polje: ```php -// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3) -$table->whereOr([ - 'id > ?' => 12, - 'ROUND(id, ?) = ?' => [5, 3], -]); +// Kjer `foo_id` = 1 IN `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); + +// Kjer (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) +$table->wherePrimary([ + ['foo_id' => 1, 'bar_id' => 5], + ['foo_id' => 2, 'bar_id' => 3], +])->fetchAll(); ``` -Naročilo() .[#toc-order] ------------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Primeri uporabe: +Določa vrstni red vrnjenih vrstic. Razvrstite lahko po enem ali več stolpcih, v naraščajočem ali padajočem vrstnem redu ali z izrazom po meri: ```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'); // NAROČI PO `ustvarjenem` +$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() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Določa stolpce, ki se vrnejo iz podatkovne zbirke. Privzeto Nette Database Explorer vrne samo stolpce, ki se dejansko uporabljajo v kodi. Metodo `select()` uporabite, kadar želite pridobiti določene izraze: + +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` -Primeri uporabe: +Vzdevki, določeni z uporabo `AS`, so nato dostopni kot lastnosti predmeta `ActiveRow`: ```php -$table->select('field1'); // SELECT `polje1` -$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; // dostop do vzdevka +} ``` -limit() .[#toc-limit] ---------------------- +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Primeri uporabe: +Omeji število vrnjenih vrstic (LIMIT) in po želji določi odmik: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (vrne prvih 10 vrstic) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Za paginacijo je primerneje uporabiti metodo `page()`. + -stran() .[#toc-page] --------------------- +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- -Alternativni način za nastavitev meje in odmika: +Poenostavi paginiranje rezultatov. Sprejme številko strani (od 1 naprej) in število elementov na strani. Po želji lahko posredujete sklic na spremenljivko, v kateri bo shranjeno skupno število strani: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Pridobitev številke zadnje strani, ki je posredovana v spremenljivko `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Poveže vrstice po določenih stolpcih (GROUP BY). Običajno se uporablja v kombinaciji z združevalnimi funkcijami: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Šteje število izdelkov v vsaki kategoriji. +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -skupina() .[#toc-group] ------------------------ +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Primeri uporabe: +Določi pogoj za filtriranje združenih vrstic (HAVING). Uporablja se lahko v kombinaciji z metodo `group()` in združevalnimi funkcijami: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Poišče kategorije z več kot 100 izdelki +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Branje podatkov +=============== + +Za branje podatkov iz podatkovne zbirke je na voljo več uporabnih metod: + +.[language-php] +| `foreach ($table as $key => $row)` | Iterira skozi vse vrstice, `$key` je vrednost primarnega ključa, `$row` je objekt ActiveRow | +| `$row = $table->get($key)` | Vrne posamezno vrstico po primarnem ključu | +| `$row = $table->fetch()` | Vrne trenutno vrstico in premakne kazalec na naslednjo | +| `$array = $table->fetchPairs()` | Iz rezultatov ustvari asociativno polje | +| `$array = $table->fetchAll()` | Vrne vse vrstice kot polje | +| `count($table)` | Vrne število vrstic v objektu Izbor | + +Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je namenjen samo branju. To pomeni, da ne morete spreminjati vrednosti njegovih lastnosti. Ta omejitev zagotavlja doslednost podatkov in preprečuje nepričakovane stranske učinke. Podatki se pridobivajo iz podatkovne zbirke, zato je treba vse spremembe izrecno in nadzorovano izvesti. + -Primeri uporabe: +`foreach` - Iteriranje skozi vse vrstice +---------------------------------------- + +Najlažji način za izvajanje poizvedbe in pridobivanje vrstic je iteracija z zanko `foreach`. Ta samodejno izvede poizvedbo SQL. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = primarni ključ, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Filtriranje po vrednosti druge tabele .[#toc-joining-key] ---------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Izvrši poizvedbo SQL in vrne vrstico po njenem primarnem ključu ali `null`, če ta ne obstaja. + +```php +$book = $explorer->table('book')->get(123); // vrne vrstico ActiveRow z ID 123 ali null +if ($book) { + echo $book->title; +} +``` -Pogosto morate rezultate filtrirati glede na pogoj, ki vključuje drugo tabelo podatkovne zbirke. Te vrste pogojev zahtevajo združitev tabel. Vendar vam jih ni treba več pisati. -Recimo, da morate dobiti vse knjige, katerih avtor je "Jon". Vse, kar morate napisati, je ključ združevanja relacije in ime stolpca v združeni tabeli. Ključ združevanja izhaja iz stolpca, ki se nanaša na tabelo, ki jo želite združiti. V našem primeru (glej shemo db) je to stolpec `author_id`, zato je dovolj, da uporabimo samo njegov prvi del - `author` (končnico `_id` lahko izpustimo). `name` je stolpec v tabeli `author`, ki bi ga radi uporabili. Pogoj za knjižni prevajalnik (ki ga povezuje stolpec `translator_id` ) lahko ustvarimo prav tako preprosto. +fetch(): ?ActiveRow .[method] +----------------------------- + +Vrne eno vrstico in premakne notranji kazalec na naslednjo. Če ni več vrstic, vrne `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); +} ``` -Logiko povezovalnega ključa poganja izvajanje funkcije [Conventions |api:Nette\Database\Conventions]. Priporočamo uporabo programa [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], ki analizira vaše tuje ključe in vam omogoča enostavno delo s temi povezavami. -Razmerje med knjigo in njenim avtorjem je 1:N. Možno je tudi obratno razmerje. Imenujemo ga **povratno povezovanje**. Oglejte si še en primer. Želimo pridobiti vse avtorje, ki so napisali več kot 3 knjige. Za obratno združevanje uporabimo izjavo `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY`, tudi pogoj mora biti zapisan v obliki izjave `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Rezultate vrne kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ v polju, drugi argument pa ime stolpca, ki se uporabi kot vrednost: ```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', ...] ``` -Morda ste opazili, da se izraz za združevanje nanaša na knjigo, vendar ni jasno, ali se združujemo prek `author_id` ali `translator_id`. V zgornjem primeru se Selection združuje prek stolpca `author_id`, ker je bilo najdeno ujemanje z izvorno tabelo - tabelo `author`. Če takšnega ujemanja ne bi bilo in bi bilo več možnosti, bi Nette vrgel [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Če je naveden samo stolpec s ključem, bo vrednost celotna vrstica, tj. objekt `ActiveRow`: -Če želite združitev opraviti prek stolpca `translator_id`, v izrazu za združevanje navedite izbirni parameter. +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +Če je kot ključ naveden `null`, bo polje številčno indeksirano od nič: ```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', ...] +``` + +Kot parameter lahko posredujete tudi povratni klic, ki bo vrnil bodisi samo vrednost bodisi par ključ-vrednost za vsako vrstico. Če povratni klic vrne samo vrednost, bo ključ primarni ključ vrstice: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Prva knjiga (Jan Novak)', ...] + +// Povratni klic lahko vrne tudi polje s parom ključ in vrednost: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['First Book' => 'Jan Novak', ...] ``` -Oglejmo si nekaj zahtevnejših izrazov za združevanje. -Radi bi našli vse avtorje, ki so napisali kaj o PHP. Vse knjige imajo oznake, zato bi morali izbrati tiste avtorje, ki so napisali katero koli knjigo z oznako PHP. +fetchAll(): array .[method] +--------------------------- + +Vrne vse vrstice kot asociativno polje objektov `ActiveRow`, kjer so ključi vrednosti primarnih ključev. ```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, ...), ...] ``` -Zbirne poizvedbe .[#toc-aggregate-queries] ------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Pridobi število vrstic -| `$table->count("DISTINCT $column")` | Pridobi število različnih vrednosti -| `$table->min($column)` | Pridobi najmanjšo vrednost -| `$table->max($column)` | Pridobi največjo vrednost -| `$table->sum($column)` | Pridobi vsoto vseh vrednosti -| `$table->aggregation("GROUP_CONCAT($column)")` | Izvedba katere koli funkcije združevanja +Metoda `count()` brez parametrov vrne število vrstic v objektu `Selection`: -.[caution] -Metoda `count()` brez določenih parametrov izbere vse zapise in vrne velikost polja, kar je zelo neučinkovito. Če morate na primer izračunati število vrstic za listanje, vedno navedite prvi argument. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativa +``` +Opomba: metoda `count()` s parametrom izvede funkcijo združevanja COUNT v zbirki podatkov, kot je opisano spodaj. -Pobeg in citiranje .[#toc-escaping-quoting] -=========================================== -Raziskovalec podatkovnih zbirk je pameten in za vas izloči parametre in identifikatorje narekovajev. Kljub temu je treba upoštevati ta osnovna pravila: +ActiveRow::toArray(): array .[method] +------------------------------------- -- ključne besede, funkcije, postopki morajo biti zapisani z velikimi črkami -- stolpci in tabele morajo biti pisani z malimi črkami -- spremenljivke se posredujejo kot parametri, ne smejo se združevati +Objekt `ActiveRow` pretvori v asociativno polje, kjer so ključi imena stolpcev, vrednosti pa ustrezni podatki. ```php -->where('name like ?', 'John'); // Napačno! generira: `name` `like` ? -->where('name LIKE ?', 'John'); // PRAVILNO +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray bo ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` -->where('KEY = ?', $value); // NAPAČNO! KEY je ključna beseda -->where('key = ?', $value); // PRAVILNO. generira: `key` = ? -->where('name = ' . $name); // NAPAKA! vbrizgavanje sql! -->where('name = ?', $name); // PRAVILNO +Agregacija .[#toc-aggregation] +============================== -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // NAPAKA! spremenljivke se posredujejo kot parametri, ne združujejo se -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // KORISTNO +Razred `Selection` ponuja metode za enostavno izvajanje funkcij združevanja (COUNT, SUM, MIN, MAX, AVG itd.). + +.[language-php] +| `count($expr)` | Šteje število vrstic | +| `min($expr)` | Vrne najmanjšo vrednost v stolpcu | +| `max($expr)` | Vrne največjo vrednost v stolpcu | +| `sum($expr)` | Vrne vsoto vrednosti v stolpcu | +| `aggregation($function)` | Omogoča katero koli funkcijo združevanja, kot sta `AVG()` ali `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Izvede poizvedbo SQL s funkcijo COUNT in vrne rezultat. Ta metoda se uporablja za ugotavljanje, koliko vrstic ustreza določenemu pogoju: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `stolpec`) FROM `table` +``` + +Opomba: [funkcija count() |#count()] brez parametra preprosto vrne število vrstic v objektu `Selection`. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Metodi `min()` in `max()` vrneta najmanjšo in največjo vrednost v navedenem stolpcu ali izrazu: + +```php +// SELECT MAX(`cena`) FROM `izdelki` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Vrne vsoto vrednosti v določenem stolpcu ali izrazu: + +```php +// SELECT SUM(`cena` * `predmeti na zalogi`) FROM `izdelki` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -nepravilna uporaba lahko povzroči varnostne luknje +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Omogoča izvajanje katere koli funkcije združevanja. -Pridobivanje podatkov .[#toc-fetching-data] -=========================================== +```php +// Izračuna povprečno ceno izdelkov v kategoriji. +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -| `foreach ($table as $id => $row)` | Iterirajte po vseh vrsticah v rezultatu -| `$row = $table->get($id)` | Pridobi posamezno vrstico z ID $id iz tabele -| `$row = $table->fetch()` | Pridobi naslednjo vrstico iz rezultata -| `$array = $table->fetchPairs($key, $value)` | Prevzem vseh vrednosti v asociativno polje -| `$array = $table->fetchPairs($value)` | Prevzem vseh vrstic v asociativno polje -| `count($table)` | Pridobi število vrstic v nizu rezultatov +// združuje oznake izdelkov v en sam niz +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Če moramo združiti rezultate, ki so sami rezultat združevanja in grupiranja (npr. `SUM(value)` nad grupiranimi vrsticami), kot drugi argument navedemo funkcijo združevanja, ki se uporabi za te vmesne rezultate: + +```php +// Izračuna skupno ceno izdelkov na zalogi za vsako kategorijo, nato pa te cene sešteje. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +V tem primeru najprej izračunamo skupno ceno izdelkov v vsaki kategoriji (`SUM(price * stock) AS category_total`) in rezultate združimo po `category_id`. Nato za seštevanje teh vmesnih vsot uporabimo `aggregation('SUM(category_total)', 'SUM')`. Drugi argument `'SUM'` določa funkcijo združevanja, ki se uporabi za vmesne rezultate. Vstavljanje, posodabljanje in brisanje .[#toc-insert-update-delete] =================================================================== -Metoda `insert()` sprejme polje objektov Traversable (na primer [ArrayHash |utils:arrays#ArrayHash], ki vrne [obrazce |forms:]): +Nette Database Explorer poenostavlja vstavljanje, posodabljanje in brisanje podatkov. Vse omenjene metode v primeru napake zavržejo sporočilo `Nette\Database\DriverException`. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Vnese nove zapise v tabelo. + +**Vstavljanje posameznega zapisa:** + +Nov zapis se posreduje kot asociativno polje ali iterabilni objekt (kot je `ArrayHash`, ki se uporablja v [obrazcih |forms:]), kjer se ključi ujemajo z imeni stolpcev v tabeli. + +Če ima tabela določen primarni ključ, metoda vrne objekt `ActiveRow`, ki se ponovno naloži iz podatkovne zbirke, da odraža vse spremembe na ravni podatkovne zbirke (npr. sprožilce, privzete vrednosti stolpcev ali izračune samodejnega povečanja). S tem je zagotovljena doslednost podatkov, objekt pa vedno vsebuje trenutne podatke iz zbirke podatkov. Če primarni ključ ni izrecno določen, metoda vrne vhodne podatke kot polje. ```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 primerek ActiveRow, ki vsebuje celotne podatke vstavljene vrstice, +// vključno s samodejno ustvarjenim ID in vsemi spremembami, ki so jih naredili sprožilci +echo $row->id; // izpiše ID novo vstavljenega uporabnika +echo $row->created_at; // izpiše čas ustvarjanja, če ga je določil sprožilec ``` -Če je v tabeli določen primarni ključ, se vrne objekt ActiveRow, ki vsebuje vstavljeno vrstico. +**Vstavljanje več zapisov naenkrat:** -Večkratno vstavljanje: +Metoda `insert()` omogoča vstavljanje več zapisov z eno samo poizvedbo SQL. V tem primeru vrne število vstavljenih vrstic. ```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 bo 2 +``` + +Kot parameter lahko posredujete tudi objekt `Selection` z izbranimi podatki. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Datoteke ali objekti DateTime se lahko posredujejo kot parametri: +**Vstavljanje posebnih vrednosti:** + +Vrednosti lahko vključujejo datoteke, predmete `DateTime` ali literale SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // ali $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // vstavi datoteko + 'name' => 'John', + 'created_at' => new DateTime, // pretvori v obliko podatkovne zbirke + 'avatar' => fopen('image.jpg', 'rb'), // vstavi vsebino binarne datoteke + 'uuid' => $explorer::literal('UUID()'), // pokliče funkcijo UUID() ]); ``` -Posodabljanje (vrne število prizadetih vrstic): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Posodobi vrstice v tabeli na podlagi določenega filtra. Vrne število dejansko spremenjenih vrstic. + +Stolpci, ki jih je treba posodobiti, se posredujejo kot asociativno polje ali iterabilni objekt (kot je `ArrayHash`, ki se uporablja v [obrazcih |forms:]), kjer se ključi ujemajo z imeni stolpcev v tabeli: ```php -$count = $explorer->table('users') - ->where('id', 10) // je treba poklicati pred funkcijo 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 ``` -Za posodabljanje lahko uporabimo operatorje `+=` a `-=`: +Za spreminjanje številskih vrednosti lahko uporabite operatorja `+=` in `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // glej += + 'points+=' => 1, // poveča vrednost stolpca "točke" za 1. + 'coins-=' => 1, // zmanjša vrednost stolpca "kovanci" za 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Brisanje (vrne število izbrisanih vrstic): + +Selection::delete(): int .[method] +---------------------------------- + +Iz tabele izbriše vrstice na podlagi določenega filtra. Vrne število izbrisanih vrstic. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +Ko kličete `update()` ali `delete()`, ne pozabite uporabiti `where()` za določitev vrstic, ki jih je treba posodobiti ali izbrisati. Če `where()` ni uporabljen, bo operacija izvedena za celotno tabelo! + -Delo z razmerji .[#toc-working-with-relationships] -================================================== +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- +Posodobi podatke v vrstici zbirke podatkov, ki jo predstavlja objekt `ActiveRow`. Kot parameter sprejme iterabilne podatke, pri čemer so ključi imena stolpcev. Za spreminjanje številskih vrednosti lahko uporabite operatorja `+=` in `-=`: -Ima eno razmerje .[#toc-has-one-relation] ------------------------------------------ -Ima eno razmerje je pogost primer uporabe. Knjiga ima enega avtorja. Knjiga ima enega* prevajalca. Pridobivanje povezanih vrstic se večinoma izvaja z metodo `ref()`. Sprejme dva argumenta: ime ciljne tabele in izvorni povezovalni stolpec. Oglejte si primer: +Po izvedeni posodobitvi se objekt `ActiveRow` samodejno ponovno naloži iz podatkovne zbirke, da odraža vse spremembe, izvedene na ravni podatkovne zbirke (npr. sprožilci). Metoda vrne `true` le, če je prišlo do dejanske spremembe podatkov. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // poveča število ogledov +]); +echo $article->views; // izpiše trenutno število ogledov ``` -V zgornjem primeru smo iz tabele `author` pridobili povezan vnos avtorja, primarni ključ avtorja pa smo poiskali po stolpcu `book.author_id`. Metoda Ref() vrne primerek ActiveRow ali nič, če ni ustreznega vnosa. Vrnjena vrstica je primerek ActiveRow, zato lahko z njo delamo enako kot z vnosom knjige. +Ta metoda posodobi samo eno določeno vrstico v zbirki podatkov. Za množične posodobitve več vrstic uporabite metodo [Selection::update(). |#Selection::update()] + + +ActiveRow::delete() .[method] +----------------------------- + +Izbriše vrstico iz zbirke podatkov, ki jo predstavlja objekt `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // izbriše knjigo z ID 1 +``` + +Ta metoda izbriše samo eno določeno vrstico v zbirki podatkov. Za množično brisanje več vrstic uporabite metodo [Selection::delete(). |#Selection::delete()] -// ali neposredno -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +Razmerja med tabelami .[#toc-relationships-between-tables] +========================================================== + +V relacijskih podatkovnih zbirkah so podatki razdeljeni v več tabel in povezani s tujimi ključi. Nette Database Explorer ponuja revolucionaren način dela s temi razmerji - brez pisanja poizvedb JOIN ali zahteve po konfiguraciji ali ustvarjanju entitet. + +Za demonstracijo bomo uporabili podatkovno zbirko **primer**[(na voljo na GitHubu |https://github.com/nette-examples/books]). Podatkovna baza vsebuje naslednje tabele: + +- `author` - avtorji in prevajalci (stolpci `id`, `name`, `web`, `born`) +- `book` - knjige (stolpci `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - oznake (stolpci `id`, `name`) +- `book_tag` - preglednica povezav med knjigami in oznakami (stolpci `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Struktura podatkovne zbirke .<> + +V tem primeru zbirke podatkov o knjigah najdemo več vrst povezav (poenostavljeno v primerjavi z resničnostjo): + +- Vsaka knjiga ima enega avtorja; avtor lahko napiše več knjig. +- **Zero-to-many (0:N)** - Knjiga ima lahko prevajalca; prevajalec lahko prevede **več** knjig. +- **Zero-to-one (0:1)** - Knjiga ima lahko** nadaljevanje. +- **Mnogo-več (M:N)** - Knjiga ima lahko več** značk in ena značka je lahko dodeljena več knjigam. + +V teh razmerjih vedno obstajata **starševska tabela** in **družinska tabela**. Na primer, v razmerju med avtorji in knjigami je tabela `author` starš, tabela `book` pa otrok - to si lahko predstavljate tako, da knjiga vedno "pripada" avtorju. To se odraža tudi v strukturi zbirke podatkov: podrejena tabela `book` vsebuje tuj ključ `author_id`, ki se sklicuje na nadrejeno tabelo `author`. + +Če želimo prikazati knjige skupaj z imeni njihovih avtorjev, imamo dve možnosti. Podatke lahko pridobimo z eno samo poizvedbo SQL s povezavo JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +ali pa podatke pridobimo v dveh korakih - najprej knjige, nato njihove avtorje - in jih sestavimo v PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Knjiga ima tudi en prevajalnik, zato je pridobivanje imena prevajalnika precej enostavno. +Drugi pristop je presenetljivo **učinkovitejši**. Podatki se pridobijo samo enkrat in jih je mogoče bolje uporabiti v predpomnilniku. Natanko tako deluje Nette Database Explorer - za vse poskrbi pod pokrovom in vam ponudi čist 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 zapis iz tabele 'author' + echo 'translated by: ' . $book->translator?->name; +} ``` -Vse to je v redu, vendar je nekoliko okorno, se vam ne zdi? Raziskovalec zbirke podatkov že vsebuje definicije tujih ključev, zakaj jih torej ne bi uporabljali samodejno? Naredimo to! -Če kličemo lastnost, ki ne obstaja, ActiveRow poskuša ime kličoče lastnosti razrešiti kot relacijo 'ima eno'. Pridobitev te lastnosti je enaka klicu metode ref() s samo enim argumentom. Edini argument bomo imenovali **ključ**. Ključ bo razrešen na določeno relacijo tujega ključa. Predani ključ se primerja s stolpci vrstice, in če se ujema, se za pridobivanje podatkov iz povezane ciljne tabele uporabi tuji ključ, ki je opredeljen v ujemajočem se stolpcu. Oglejte si primer: +Dostop do matične tabele .[#toc-accessing-the-parent-table] +----------------------------------------------------------- + +Dostop do nadrejene tabele je preprost. To so razmerja, kot sta *knjiga ima avtorja* ali *knjiga ima lahko prevajalca*. Do povezanega zapisa lahko dostopamo prek lastnosti predmeta `ActiveRow` - ime lastnosti se ujema z imenom stolpca tujega ključa brez končnice `id`: ```php -$book->author->name; -// enako kot -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // poišče avtorja prek stolpca 'author_id'. +echo $book->translator?->name; // poišče prevajalca prek stolpca "translator_id". ``` -Primer: Primerek ActiveRow nima stolpca avtor. V vseh stolpcih knjige se išče ujemanje s *ključ*. Ujemanje v tem primeru pomeni, da mora ime stolpca vsebovati ključ. V zgornjem primeru torej stolpec `author_id` vsebuje niz "avtor" in se zato ujema s ključem "avtor". Če želite pridobiti prevajalca knjige, lahko kot ključ uporabite npr. 'translator', saj se bo ključ 'translator' ujemal s stolpcem `translator_id`. Več o logiki ujemanja ključev najdete v poglavju [Združevanje izrazov |#joining-key]. +Pri dostopu do lastnosti `$book->author` Raziskovalec išče stolpec v tabeli `book`, ki vsebuje niz `author` (npr. `author_id`). Na podlagi vrednosti v tem stolpcu pridobi ustrezen zapis iz tabele `author` in ga vrne kot objekt `ActiveRow`. Podobno `$book->translator` uporablja stolpec `translator_id`. Ker lahko stolpec `translator_id` vsebuje `null`, se uporabi operator `?->`. + +Alternativni pristop ponuja metoda `ref()`, ki sprejme dva argumenta - ime ciljne tabele in povezovalni stolpec - in vrne primerek `ActiveRow` ali `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; // povezava do avtorja +echo $book->ref('author', 'translator_id')->name; // povezava do prevajalca ``` -Če želite pridobiti več knjig, morate uporabiti enak pristop. Nette Database Explorer bo poiskal avtorje in prevajalce za vse poiskane knjige naenkrat. +Metoda `ref()` je uporabna, če ni mogoče uporabiti dostopa na podlagi lastnosti, na primer kadar tabela vsebuje stolpec z enakim imenom kot lastnost (`author`). V drugih primerih je zaradi boljše berljivosti priporočljiva uporaba dostopa na podlagi lastnosti. + +Raziskovalec samodejno optimizira poizvedbe po zbirki podatkov. Pri iteraciji skozi knjige in dostopu do njihovih povezanih zapisov (avtorji, prevajalci) Raziskovalec ne ustvari poizvedbe za vsako knjigo posebej. Namesto tega za vsako vrsto povezave** izvede le **eno poizvedbo SELECT, s čimer znatno zmanjša obremenitev podatkovne zbirke. Na primer: ```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; } ``` -Koda bo izvedla samo te tri poizvedbe: +Ta koda bo izvedla le tri optimizirane poizvedbe po zbirki podatkov: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Logika za identifikacijo povezovalnega stolpca je opredeljena z implementacijo [Conventions |api:Nette\Database\Conventions]. Priporočamo uporabo [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], ki analizira tuje ključe in omogoča nemoteno delo z obstoječimi povezavami tabel. -Ima veliko relacij .[#toc-has-many-relation] --------------------------------------------- -Razmerje "ima veliko" je samo obrnjeno razmerje "ima eno". Avtor je napisal veliko knjig. Avtor *je* prevedel *mnogo* knjig. Kot lahko vidite, je ta vrsta relacije nekoliko težja, saj je relacija 'poimenovana' ('napisal', 'prevedel'). Primer ActiveRow ima metodo `related()`, ki vrne polje povezanih vnosov. Vnosi so prav tako primerki ActiveRow. Oglejte si primer spodaj: +Dostop do podrejene tabele .[#toc-accessing-the-child-table] +------------------------------------------------------------ + +Dostop do podrejene tabele deluje v nasprotni smeri. Zdaj vprašamo *katero knjigo je napisal ta avtor* ali *katero knjigo je prevedel ta prevajalec*. Za takšno poizvedbo uporabimo metodo `related()`, ki vrne objekt `Selection` s povezanimi zapisi. Tukaj je primer: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Izpiše vse knjige, ki jih je napisal avtor foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Izhodi vse knjige, ki jih je avtor prevedel foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Metoda `related()` Metoda sprejme celoten opis združevanja, posredovan kot dva argumenta ali kot en argument, združen s piko. Prvi argument je ciljna tabela, drugi pa ciljni stolpec. +Metoda `related()` sprejme opis povezave kot en sam argument z uporabo pike ali kot dva ločena argumenta: ```php -$author->related('book.translator_id'); -// enako kot -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // en sam argument +$author->related('book', 'translator_id'); // dva argumenta ``` -Uporabite lahko hevristiko Nette Database Explorerja, ki temelji na tujih ključih, in navedete samo argument **ključ**. Ključ bo primerjan z vsemi tujimi ključi, ki kažejo na trenutno tabelo (`author` tabela). Če se ujemajo, bo Nette Database Explorer uporabil ta tuji ključ, v nasprotnem primeru bo vrgel [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] ali [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Več o logiki ujemanja ključev najdete v poglavju [Združevanje izrazov |#joining-key]. +Raziskovalec lahko samodejno zazna pravilen povezovalni stolpec na podlagi imena nadrejene tabele. V tem primeru se poveže prek stolpca `book.author_id`, ker je ime izvorne tabele `author`: -Seveda lahko pokličete sorodne metode za vse pridobljene avtorje, Nette Database Explorer pa bo ponovno pridobil ustrezne knjige naenkrat. +```php +$author->related('book'); // uporablja book.author_id +``` + +Če obstaja več možnih povezav, bo Raziskovalec vrgel izjemo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Seveda lahko metodo `related()` uporabimo tudi pri iteraciji skozi več zapisov v zanki in Raziskovalec bo tudi v tem primeru samodejno optimiziral poizvedbe: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Zgornji primer bo izvedel samo dve poizvedbi: +Ta koda ustvari le dve učinkoviti poizvedbi SQL: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Ročno ustvarjanje raziskovalca .[#toc-creating-explorer-manually] -================================================================= +Razmerje veliko-več (Many-to-Many) .[#toc-many-to-many-relationship] +-------------------------------------------------------------------- + +Za razmerje mnogo-več (M:N) je potrebna **skupna tabela** (v našem primeru `book_tag`). Ta tabela vsebuje dva stolpca tujih ključev (`book_id`, `tag_id`). Vsak stolpec se sklicuje na primarni ključ ene od povezanih tabel. Če želimo pridobiti povezane podatke, najprej pridobimo zapise iz povezovalne tabele z uporabo `related('book_tag')`, nato pa nadaljujemo do ciljnih podatkov: + +```php +$book = $explorer->table('book')->get(1); +// Izpiše imena oznak, dodeljenih knjigi +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // pridobi ime oznake prek preglednice povezav +} + +$tag = $explorer->table('tag')->get(1); +// Nasprotna smer: izpiše naslove knjig s to oznako +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // pridobi naslov knjige +} +``` -Povezavo s podatkovno zbirko lahko ustvarite s konfiguracijo aplikacije. V takih primerih se ustvari storitev `Nette\Database\Explorer`, ki jo je mogoče posredovati kot odvisnost z uporabo vsebnika DI. +Raziskovalec ponovno optimizira poizvedbe SQL v učinkovito obliko: -Če pa se Nette Database Explorer uporablja kot samostojno orodje, je treba primerek objekta `Nette\Database\Explorer` ustvariti ročno. +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Poizvedovanje po sorodnih tabelah .[#toc-querying-through-related-tables] +------------------------------------------------------------------------- + +V metodah `where()`, `select()`, `order()` in `group()` lahko uporabite posebne zapise za dostop do stolpcev iz drugih tabel. Raziskovalec samodejno ustvari potrebne povezave JOIN. + +**Zaznamek s piko** (`parent_table.column`) se uporablja za razmerja 1:N, gledano z vidika nadrejene tabele: ```php -// $storage implements Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// poišče knjige, katerih imena avtorjev se začnejo z 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// razvrsti knjige po imenu avtorja padajoče. +$books->order('author.name DESC'); + +// izpiše naslov knjige in ime avtorja +$books->select('book.title, author.name'); +``` + +Za razmerja 1:N z vidika nadrejene tabele se uporablja **zapis v stolpcu**: + +```php +$authors = $explorer->table('author'); + +// Poišče avtorje, ki so napisali knjigo z besedo 'PHP' v naslovu +$authors->where(':book.title LIKE ?', '%PHP%'); + +// šteje število knjig za vsakega avtorja +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +V zgornjem primeru z zapisom v dvopičju (`:book.title`) stolpec tujega ključa ni izrecno določen. Raziskovalec samodejno zazna pravilen stolpec na podlagi imena nadrejene tabele. V tem primeru se poveže prek stolpca `book.author_id`, ker je ime izvorne tabele `author`. Če obstaja več možnih povezav, Raziskovalec vrže izjemo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Povezovalni stolpec je lahko izrecno naveden v oklepajih: + +```php +// Poišče avtorje, ki so prevedli knjigo z besedo 'PHP' v naslovu +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Za dostop do podatkov v več tabelah je mogoče zapise verižno povezati: + +```php +// Poišče avtorje knjig z oznako 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Razširitev pogojev za JOIN .[#toc-extending-conditions-for-join] +---------------------------------------------------------------- + +Metoda `joinWhere()` dodaja dodatne pogoje za združevanje tabel v jeziku SQL za ključno besedo `ON`. + +Recimo, da želimo poiskati knjige, ki jih je prevedel določen prevajalec: + +```php +// Poišče knjige, ki jih je prevedel prevajalec z imenom 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEVA povezava z avtorjem prevajalcem ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +V pogoju `joinWhere()` lahko uporabite enake konstrukcije kot v metodi `where()` - operatorje, nadomestne znake, polja vrednosti ali izraze SQL. + +Za bolj zapletene poizvedbe z več povezavami JOIN lahko določite namizne vzdevke: + +```php +$tags = $explorer->table('tag') + ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) + ->alias(':book_tag.book.author', 'book_author'); +// LEVO VEZI `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` +// LEVI JOIN `knjiga` ON `knjiga_tag`.`knjiga_id` = `knjiga`.`id` +// LEVI JOIN `autor` `book_author` ON `book`.`author_id` = `book_author`.`id` +// AND (`book_author`.`born` < 1950) +``` + +Upoštevajte, da metoda `where()` dodaja pogoje v stavek `WHERE`, metoda `joinWhere()` pa razširja pogoje v stavku `ON` med združevanjem tabel. + + +Ročno ustvarjanje raziskovalca .[#toc-manually-creating-explorer] +================================================================= + +Če ne uporabljate vsebnika Nette DI, lahko primerek `Nette\Database\Explorer` ustvarite ročno: + +```php +use Nette\Database; + +// $storage implementira Nette\Caching\Storage, npr.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// povezava s podatkovno bazo +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// upravlja odsev strukture podatkovne zbirke +$structure = new Database\Structure($connection, $storage); +// določa pravila za preslikavo imen tabel, stolpcev in tujih ključev +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/sl/security.texy b/database/sl/security.texy new file mode 100644 index 0000000000..7d16716d5b --- /dev/null +++ b/database/sl/security.texy @@ -0,0 +1,145 @@ +Varnostna tveganja +****************** + +.[perex] +Podatkovne zbirke pogosto vsebujejo občutljive podatke in omogočajo nevarne operacije. Podatkovna baza Nette zagotavlja številne varnostne funkcije. Vendar je ključnega pomena, da razumete razliko med varnimi in nevarnimi vmesniki API. + + +Vsiljevanje SQL .[#toc-sql-injection] +===================================== + +Vbrizgavanje SQL je najresnejše varnostno tveganje pri delu s podatkovnimi zbirkami. Pojavi se, ko nepreverjen uporabniški vnos postane del poizvedbe SQL. Napadalec lahko vnese svoje ukaze SQL in tako pridobi ali spremeni podatke v zbirki podatkov. + +```php +// ❌ NEVARNA KODA - ranljiva za vbrizgavanje SQL +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Napadalec lahko vnese nekaj takega: ' ALI '1'='1 +// Rezultat poizvedbe bo: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// To vrne vse uporabnike! +``` + +Enako velja za Raziskovalca podatkovnih zbirk: + +```php +// ❌ KODEKS NEVARNOSTI +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Varne parametrirane poizvedbe .[#toc-safe-parameterized-queries] +================================================================ + +Varen način za vstavljanje vrednosti v poizvedbe SQL je uporaba parametriziranih poizvedb. Podatkovna baza Nette ponuja več načinov za njihovo uporabo. + + +Namestni vprašalni znaki .[#toc-placeholder-question-marks] +----------------------------------------------------------- + +Najpreprostejša metoda je uporaba nadomestnih vprašalnih znakov: + +```php +// ✅ Varne parametrizirane poizvedbe +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Varno stanje v Raziskovalcu +$table->where('name = ?', $_GET['name']); +``` + +Enako velja za vse druge metode v Raziskovalcu podatkovne baze, ki omogočajo vstavljanje izrazov z nadomestnimi vprašalnimi znaki in parametri. + +.[warning] +Vrednosti morajo biti skalarnega tipa (`string`, `int`, `float`, `bool`) ali `null`. Če npr, `$_GET['name']` je polje, bo podatkovna baza Nette v poizvedbo SQL vključila vse njegove elemente, kar je lahko nezaželeno. + + +Polja vrednosti .[#toc-value-arrays] +------------------------------------ + +Za stavke `INSERT`, `UPDATE` ali `WHERE` lahko uporabimo polja vrednosti: + +```php +// ✅ Safe INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ varen UPDATE +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Podatkovna zbirka Nette samodejno izloči vse vrednosti, posredovane prek parametriziranih poizvedb. Kljub temu moramo zagotoviti pravilno podatkovno vrsto parametrov. + + +Ključi matrike niso varen API .[#toc-array-keys-are-not-a-safe-api] +=================================================================== + +Medtem ko so vrednosti v poljih varne, tega ne moremo trditi za ključe: + +```php +// ❌ NEVARNA KODA - ključi lahko vsebujejo SQL injekcijo +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Za ukaze `INSERT` in `UPDATE` je to kritična varnostna pomanjkljivost - napadalec lahko vstavi ali spremeni kateri koli stolpec v zbirki podatkov. Tako bi lahko na primer nastavil `is_admin = 1` ali vstavil poljubne podatke v občutljive stolpce. + +Pri pogojih za ukaz `WHERE` je to še bolj nevarno, saj omogoča **izpolnjevanje podatkovne zbirke** - tehniko za postopno pridobivanje informacij o podatkovni zbirki. Napadalec bi lahko poskušal raziskati plače zaposlenih tako, da bi vnesel v `$_GET` takole: + +```php +$_GET = ['salary >', 100000]; // začne določati plačne razrede. +``` + +Glavna težava pa je, da pogoji `WHERE` podpirajo izraze SQL v ključih: + +```php +// Zakonita uporaba operatorjev v ključih +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ NEvarno: napadalec lahko vnese svoj SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // omogoča napadalcu, da pridobi administratorske plače +``` + +To je ponovno **vbrizgavanje SQL**. + + +Dodajanje stolpcev na belo listo .[#toc-whitelisting-columns] +------------------------------------------------------------- + +Če želite uporabnikom dovoliti izbiro stolpcev, vedno uporabite beli seznam: + +```php +// ✅ Varna obdelava - samo dovoljeni stolpci +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dinamični identifikatorji .[#toc-dynamic-identifiers] +===================================================== + +Za dinamična imena tabel in stolpcev uporabite nosilec `?name`: + +```php +// ✅ Varna uporaba zaupanja vrednih identifikatorjev +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ UNSAFE - nikoli ne uporabljajte uporabniškega vnosa +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Simbol `?name` se sme uporabljati samo za zaupanja vredne vrednosti, določene v aplikacijski kodi. Za vrednosti, ki jih določi uporabnik, ponovno uporabite beli seznam. diff --git a/database/tr/@left-menu.texy b/database/tr/@left-menu.texy index 22242018c0..6edf9c494a 100644 --- a/database/tr/@left-menu.texy +++ b/database/tr/@left-menu.texy @@ -4,3 +4,4 @@ Veritabanı - [Kaşif |Explorer] - [Yansıma |Reflection] - [Konfigürasyon |Configuration] +- [Güvenlik Riskleri |security] diff --git a/database/tr/explorer.texy b/database/tr/explorer.texy index 76194e4f1b..304e4ede04 100644 --- a/database/tr/explorer.texy +++ b/database/tr/explorer.texy @@ -3,548 +3,927 @@ Veritabanı Gezgini
-Nette Database Explorer, SQL sorguları yazmadan veritabanından veri almayı önemli ölçüde kolaylaştırır. +Nette Database Explorer, SQL sorguları yazmaya gerek kalmadan veritabanından veri alımını önemli ölçüde basitleştiren güçlü bir katmandır. -- verimli sorgular kullanır -- hiçbir veri gereksiz yere iletilmez -- zarif sözdizimine sahiptir +- Verilerle çalışmak doğaldır ve anlaşılması kolaydır +- Yalnızca gerekli verileri getiren optimize edilmiş SQL sorguları oluşturur +- JOIN sorguları yazmaya gerek kalmadan ilgili verilere kolay erişim sağlar +- Herhangi bir yapılandırma veya varlık oluşturma olmadan hemen çalışır
-Veritabanı Gezgini'ni kullanmak için bir tablo ile başlayın - [api:Nette\Database\Explorer] nesnesi üzerinde `table()` adresini çağırın. Bir bağlam nesnesi örneği elde etmenin en kolay yolu [burada açıklanmıştır |core#Connection and Configuration] veya Nette Database Explorer'ın bağımsız bir araç olarak kullanılması durumunda, [manuel |#Creating Explorer Manually] olarak [oluşturulabilir |#Creating Explorer Manually]. +Nette Database Explorer, veritabanı yönetimine uygun bir nesne yönelimli yaklaşım ekleyen düşük seviyeli [Nette Database Core |core] katmanının bir uzantısıdır. + +Explorer ile çalışmak, [api:Nette\Database\Explorer] nesnesi üzerinde `table()` yöntemini çağırmakla başlar (nasıl elde edileceği [burada açıklanmıştır |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // db tablo adı 'book' +$books = $explorer->table('book'); // 'book' tablo adıdır ``` -Çağrı, tüm kitapları almak için üzerinde yinelenebilen bir [Selection |api:Nette\Database\Table\Selection] nesnesi örneği döndürür. Her öğe (bir satır), özelliklerine eşlenen verilerle birlikte bir [ActiveRow |api:Nette\Database\Table\ActiveRow] örneği ile temsil edilir: +Yöntem, bir SQL sorgusunu temsil eden bir [Selection |api:Nette\Database\Table\Selection] nesnesi döndürür. Sonuçları filtrelemek ve sıralamak için bu nesneye ek yöntemler zincirlenebilir. Sorgu yalnızca veriler istendiğinde, örneğin `foreach` ile yinelenerek bir araya getirilir ve yürütülür. Her satır bir [ActiveRow |api:Nette\Database\Table\ActiveRow] nesnesi ile temsil edilir: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // 'başlık' sütunu çıktıları + echo $book->author_id; // 'author_id' sütununu çıktılar } ``` -Sadece belirli bir satırı almak, doğrudan bir ActiveRow örneği döndüren `get()` yöntemiyle yapılır. +Explorer, [tablo ilişkileriyle |#Vazby mezi tabulkami] çalışmayı büyük ölçüde kolaylaştırır. Aşağıdaki örnek, ilgili tablolardan (kitaplar ve yazarları) ne kadar kolay veri çıktısı alabileceğimizi göstermektedir. JOIN sorgularının yazılmasına gerek olmadığına dikkat edin; Nette bunları bizim için oluşturur: ```php -$book = $explorer->table('book')->get(2); // kimliği 2 olan kitabı döndürür -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // 'author' tablosuna bir JOIN oluşturur +} ``` -Yaygın kullanım durumuna bir göz atalım. Kitapları ve yazarlarını getirmeniz gerekiyor. Bu yaygın bir 1:N ilişkisidir. Sık kullanılan çözüm, tablo birleştirmeleri ile tek bir SQL sorgusu kullanarak veri getirmektir. İkinci olasılık, verileri ayrı ayrı almak, kitapları almak için bir sorgu çalıştırmak ve ardından başka bir sorgu ile her kitap için bir yazar almaktır (örneğin foreach döngünüzde). Bu, biri kitaplar için diğeri de gerekli yazarlar için olmak üzere yalnızca iki sorgu çalıştıracak şekilde kolayca optimize edilebilir - ve Nette Database Explorer'ın yaptığı da tam olarak budur. +Nette Database Explorer sorguları maksimum verimlilik için optimize eder. Yukarıdaki örnek, 10 veya 10.000 kitap işlememizden bağımsız olarak yalnızca iki SELECT sorgusu gerçekleştirir. -Aşağıdaki örneklerde, şekildeki veritabanı şeması ile çalışacağız. Kitap ve etiketleri arasında OneHasMany (1:N) bağlantıları (kitabın yazarı `author_id` ve olası çevirmeni `translator_id`, `null` olabilir) ve ManyHasMany (M:N) bağlantıları vardır. +Ayrıca Explorer, kodda hangi sütunların kullanıldığını izler ve veritabanından yalnızca bunları getirerek daha fazla performans tasarrufu sağlar. Bu davranış tamamen otomatik ve uyarlanabilirdir. Daha sonra kodu ek sütunlar kullanacak şekilde değiştirirseniz, Explorer sorguları otomatik olarak ayarlar. Hiçbir şeyi yapılandırmanıza veya hangi sütunlara ihtiyaç duyulacağını düşünmenize gerek yok - bunu Nette'e bırakın. -[Şema da dahil olmak üzere bir örnek GitHub'da bulunabilir |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Örneklerde kullanılan veritabanı yapısı .<> +Filtreleme ve Sıralama .[#toc-filtering-and-sorting] +==================================================== -Aşağıdaki kod her kitap için yazarın adını ve tüm etiketlerini listeler. Bunun dahili olarak nasıl çalıştığını birazdan [tartışacağız |#Working with relationships]. + `Selection` sınıfı, verileri filtrelemek ve sıralamak için yöntemler sağlar. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Bir WHERE koşulu ekler. Birden fazla koşul AND kullanılarak birleştirilir | +| `whereOr(array $conditions)` | OR kullanılarak birleştirilmiş bir grup WHERE koşulu ekler | +| `wherePrimary($value)` | Birincil anahtarı temel alan bir WHERE koşulu ekler | +| `order($columns, ...$params)` | ORDER BY ile sıralamayı ayarlar | +| `select($columns, ...$params)` | Hangi sütunların getirileceğini belirtir | +| `limit($limit, $offset = null)` | Satır sayısını sınırlar (LIMIT) ve isteğe bağlı olarak OFFSET ayarlar +| `page($page, $itemsPerPage, &$total = null)` | Sayfalandırmayı ayarlar | +| `group($columns, ...$params)` | Satırları gruplar (GROUP BY) | +| `having($condition, ...$params)`| Gruplanmış satırları filtrelemek için bir HAVING koşulu ekler | -foreach ($books as $book) { - echo 'başlık: ' . $kitap->başlık; - echo 'tarafından yazıldı: ' . $book->author->name; // $book->author is row from table 'author' +Yöntemler zincirlenebilir ( [akıcı arayüz |nette:introduction-to-object-oriented-programming#fluent-interfaces] olarak adlandırılır): `$table->where(...)->order(...)->limit(...)`. - echo 'etiketler: '; - foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name . ', '; // $bookTag->tag 'tag' tablosundan bir satırdır - } -} -``` +Bu yöntemler ayrıca [ilgili tablolardan verilere |#Dotazování přes související tabulky] erişmek için özel gösterimlerin kullanılmasına izin verir. -Veritabanı katmanının ne kadar verimli çalıştığını görünce memnun olacaksınız. Yukarıdaki örnek, aşağıdaki gibi görünen sabit sayıda istek yapar: -```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)) -``` +Kaçış ve Tanımlayıcılar .[#toc-escaping-and-identifiers] +-------------------------------------------------------- -[Önbellek |caching:] kullanırsanız (varsayılan olarak açık), hiçbir sütun gereksiz yere sorgulanmayacaktır. İlk sorgudan sonra, önbellek kullanılan sütun adlarını depolayacak ve Nette Database Explorer yalnızca gerekli sütunlarla sorguları çalıştıracaktır: +Yöntemler parametreleri ve alıntı tanımlayıcılarını (tablo ve sütun adları) otomatik olarak kaçarak SQL enjeksiyonunu önler. Düzgün çalışmayı sağlamak için birkaç kurala uyulmalıdır: -```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)) +- Anahtar sözcükleri, işlev adlarını, yordamları vb. **büyük harfle** yazın. +- Sütun ve tablo adlarını **küçük harfle** yazın. +- Dizeleri her zaman **parameters** kullanarak geçirin. + +```php +where('name = ' . $name); // **DISASTER**: SQL enjeksiyonuna karşı savunmasız +where('name LIKE "%search%"'); // **YANLIŞ**: otomatik alıntılamayı zorlaştırır +where('name LIKE ?', '%search%'); // **CORRECT**: parametre olarak geçirilen değer + +where('name like ?', $name); // **YANLIŞ**: üretir: `name` `like` ? +where('name LIKE ?', $name); // **DOĞRU**: üretir: isim` GİBİ ? +where('LOWER(name) = ?', $value);// **DOĞRU**: LOWER(`name`) = ? ``` -Seçmeler .[#toc-selections] -=========================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Satırların nasıl filtreleneceği ve kısıtlanacağı ile ilgili olasılıklara bakın [api:Nette\Database\Table\Selection]: +WHERE koşullarını kullanarak sonuçları filtreler. Gücü, çeşitli değer türlerini akıllıca ele almasında ve SQL operatörlerini otomatik olarak seçmesinde yatmaktadır. -.[language-php] -| `$table->where($where[, $param[, ...]])` | İki veya daha fazla koşul sağlandığında AND öğesini yapıştırıcı olarak kullanarak WHERE öğesini ayarlayın -| `$table->whereOr($where)` | İki veya daha fazla koşul sağlandığında VEYA'yı yapıştırıcı olarak kullanarak WHERE'i ayarlayın -| `$table->order($columns)` | ORDER BY ayarla, ifade olabilir `('column DESC, id DESC')` -| `$table->select($columns)` | Alınan sütunları ayarlayın, ifade olabilir `('col, MD5(col) AS hash')` -| `$table->limit($limit[, $offset])` | LIMIT ve OFFSET'i Ayarla -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Sayfalamayı etkinleştirir -| `$table->group($columns)` | GROUP BY ayarla -| `$table->having($having)` | Set HAVING +Temel kullanım: -[Akıcı arayüz |nette:introduction-to-object-oriented-programming#fluent-interfaces] olarak adlandırılan bir [arayüz |nette:introduction-to-object-oriented-programming#fluent-interfaces] kullanabiliriz, örneğin `$table->where(...)->order(...)->limit(...)`. Birden fazla `where` veya `whereOr` koşulu `AND` operatörü ile birbirine bağlanır. +```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' +``` +Uygun operatörlerin otomatik olarak algılanması sayesinde, özel durumlarla uğraşmanıza gerek kalmaz - Nette bunları sizin için halleder: -nerede() .[#toc-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) +// Yer tutucu ? operatör olmadan kullanılabilir: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer, geçirilen değerler için gerekli operatörleri otomatik olarak ekleyebilir: +Yöntem ayrıca negatif koşulları ve boş dizileri de doğru şekilde işler: -.[language-php] -| `$table->where('field', $value)` | alan = $değer -| `$table->where('field', null)` | alan NULL -| `$table->where('field > ?', $val)` | alan > $val -| `$table->where('field', [1, 2])` | alan 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 -- hiçbir şey bulamaz +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- her şeyi bulur +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- her şeyi bulur +// $table->where('NOT id ?', $ids); // UYARI: Bu sözdizimi desteklenmemektedir +``` -Sütun operatörü olmadan da yer tutucu sağlayabilirsiniz. Bu çağrılar aynıdır. +Ayrıca, bir alt sorgu oluşturarak başka bir tablo sorgusunun sonucunu parametre olarak aktarabilirsiniz: ```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')); ``` -Bu özellik, değere bağlı olarak doğru operatörün oluşturulmasını sağlar: +Koşullar, AND kullanılarak birleştirilen öğelerle birlikte bir dizi olarak da geçirilebilir: ```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', +]); ``` -Seçim, negatif koşulları da doğru bir şekilde işler, boş diziler için de çalışır: +Dizide, anahtar-değer çiftleri kullanılabilir ve Nette yine otomatik olarak doğru operatörleri seçecektir: ```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], +]); +``` -// this will throws an exception, this syntax is not supported -$table->where('NOT id ?', $ids); +SQL ifadelerini yer tutucular ve çoklu parametrelerle de karıştırabiliriz. Bu, kesin olarak tanımlanmış operatörlere sahip karmaşık koşullar için kullanışlıdır: + +```php +// WHERE (`yaş` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // iki parametre bir dizi olarak geçirilir +]); ``` + `where()` adresine yapılan birden fazla çağrı, AND kullanarak koşulları otomatik olarak birleştirir. + -whereOr() .[#toc-whereor] -------------------------- +whereOr(array $parameters): static .[method] +-------------------------------------------- -Parametresiz kullanım örneği: + `where()` adresine benzer, ancak OR kullanarak koşulları birleştirir: ```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, ]); ``` -Parametreleri kullanırız. Bir operatör belirtmezseniz, Nette Database Explorer uygun olanı otomatik olarak ekleyecektir: +Daha karmaşık ifadeler de kullanılabilir: ```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, ]); ``` -Anahtar, joker soru işaretleri içeren bir ifade içerebilir ve ardından değerde parametreleri iletebilir: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Tablonun birincil anahtarı için bir koşul ekler: ```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]); +``` + +Tablonun bileşik bir birincil anahtarı varsa (örneğin, `foo_id`, `bar_id`), bunu bir dizi olarak iletiriz: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Kullanım örnekleri: +Satırların döndürüleceği sırayı belirtir. Bir veya daha fazla sütuna göre, artan veya azalan sırada veya özel bir ifadeye göre sıralayabilirsiniz: ```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 `oluşturulan` +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDER BY `öncelik` DESC, `oluşturuldu` +$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC ``` -select() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Veritabanından döndürülecek sütunları belirtir. Varsayılan olarak, Nette Database Explorer yalnızca kodda gerçekten kullanılan sütunları döndürür. Belirli ifadeleri almanız gerektiğinde `select()` yöntemini kullanın: + +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` -Kullanım örnekleri: + `AS` kullanılarak tanımlanan takma adlara daha sonra `ActiveRow` nesnesinin özellikleri olarak erişilebilir: ```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; // takma ada erişim +} ``` -limit() .[#toc-limit] ---------------------- +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- -Kullanım örnekleri: +Döndürülen satır sayısını sınırlar (LIMIT) ve isteğe bağlı olarak bir ofset ayarlar: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (ilk 10 satırı döndürür) +$table->limit(10, 20); // LIMIT 10 OFSET 20 ``` +Sayfalandırma için `page()` yöntemini kullanmak daha uygundur. -page() .[#toc-page] -------------------- -Limit ve ofseti ayarlamak için alternatif bir yol: +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Sonuçların sayfalandırılmasını basitleştirir. Sayfa numarasını (1'den başlayarak) ve sayfa başına öğe sayısını kabul eder. İsteğe bağlı olarak, toplam sayfa sayısının saklanacağı bir değişkene referans geçebilirsiniz: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -`$lastPage` değişkenine aktarılan son sayfa numarasını alır: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Satırları belirtilen sütunlara göre gruplar (GROUP BY). Tipik olarak toplama fonksiyonları ile birlikte kullanılır: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Her kategorideki ürün sayısını sayar +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -grup() .[#toc-group] --------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Kullanım örnekleri: +Gruplandırılmış satırları filtrelemek için bir koşul belirler (HAVING). `group()` yöntemi ve toplama işlevleri ile birlikte kullanılabilir: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// 100'den fazla ürün içeren kategorileri bulur +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -sahip olmak() .[#toc-having] ----------------------------- +Veri Okuma +========== + +Veritabanından veri okumak için birkaç kullanışlı yöntem mevcuttur: + +.[language-php] +| `foreach ($table as $key => $row)` | Tüm satırlar arasında yineleme yapar, `$key` birincil anahtar değeridir, `$row` bir ActiveRow nesnesidir | +| `$row = $table->get($key)` | Birincil anahtara göre tek bir satır döndürür | +| `$row = $table->fetch()` | Geçerli satırı döndürür ve işaretçiyi bir sonrakine ilerletir | +| `$array = $table->fetchPairs()` | Sonuçlardan ilişkisel bir dizi oluşturur | +| `$array = $table->fetchAll()` | Tüm satırları bir dizi olarak döndürür | +| `count($table)` | Seçim nesnesindeki satır sayısını döndürür | + + [ActiveRow |api:Nette\Database\Table\ActiveRow] nesnesi salt okunurdur. Bu, özelliklerinin değerlerini değiştiremeyeceğiniz anlamına gelir. Bu kısıtlama, veri tutarlılığını sağlar ve beklenmedik yan etkileri önler. Veriler veritabanından alınır ve herhangi bir değişiklik açıkça ve kontrollü bir şekilde yapılmalıdır. + + +`foreach` - Tüm Satırlar Arasında Yineleme +------------------------------------------ -Kullanım örnekleri: +Bir sorguyu çalıştırmanın ve satırları almanın en kolay yolu `foreach` döngüsü ile yinelemektir. SQL sorgusunu otomatik olarak yürütür. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = birincil anahtar, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Başka Bir Tablo Değerine Göre Filtreleme .[#toc-joining-key] ------------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Bir SQL sorgusu çalıştırır ve bir satırı birincil anahtarına göre veya yoksa `null` adresine döndürür. + +```php +$book = $explorer->table('book')->get(123); // ID 123 veya null ile ActiveRow döndürür +if ($book) { + echo $book->title; +} +``` -Çoğu zaman, sonuçları başka bir veritabanı tablosunu içeren bazı koşullara göre filtrelemeniz gerekir. Bu tür koşullar tablo birleştirme gerektirir. Ancak, artık bunları yazmanıza gerek yoktur. -Diyelim ki yazarının adı 'Jon' olan tüm kitapları almanız gerekiyor. Yazmanız gereken tek şey ilişkinin birleştirme anahtarı ve birleştirilen tablodaki sütun adıdır. Birleştirme anahtarı, birleştirmek istediğiniz tabloyu ifade eden sütundan türetilir. Örneğimizde (db şemasına bakın) bu `author_id` sütunudur ve sadece ilk kısmını kullanmak yeterlidir - `author` ( `_id` son eki atlanabilir). `name`, kullanmak istediğimiz `author` tablosundaki bir sütundur. Kitap çevirmeni için bir koşul ( `translator_id` sütunu ile bağlantılı olan) aynı kolaylıkla oluşturulabilir. +fetch(): ?ActiveRow .[method] +----------------------------- + +Bir satır döndürür ve dahili işaretçiyi bir sonrakine ilerletir. Başka satır yoksa `null` döndürür. ```php $books = $explorer->table('book'); -$books->where('author.name LIKE ?', '%Jon%'); -$books->where('translator.name', 'David Grudl'); +while ($book = $books->fetch()) { + $this->processBook($book); +} ``` -Birleştirme anahtarı mantığı, [Conventions |api:Nette\Database\Conventions] uygulaması tarafından yönlendirilir. Yabancı anahtarlarınızı analiz eden ve bu ilişkilerle kolayca çalışmanıza olanak tanıyan [DiscoveredConventions'ı |api:Nette\Database\Conventions\DiscoveredConventions] kullanmanızı öneririz. -Kitap ve yazarı arasındaki ilişki 1:N'dir. Ters ilişki de mümkündür. Biz buna **backjoin** diyoruz. Başka bir örneğe bakalım. 3'ten fazla kitap yazmış olan tüm yazarları getirmek istiyoruz. Birleştirmeyi tersine çevirmek için `:` (colon). Colon means that the joined relationship means hasMany (and it's quite logical too, as two dots are more than one dot). Unfortunately, the Selection class isn't smart enough, so we have to help with the aggregation and provide a `GROUP BY` deyimini kullanırız, ayrıca koşul `HAVING` deyimi şeklinde yazılmalıdır. +fetchPairs(): array .[method] +----------------------------- + +Sonuçları ilişkisel bir dizi olarak döndürür. İlk bağımsız değişken dizide anahtar olarak kullanılacak sütun adını, ikinci bağımsız değişken ise değer olarak kullanılacak sütun adını belirtir: ```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', ...] +``` + +Yalnızca anahtar sütunu belirtilirse, değer tüm satır, yani `ActiveRow` nesnesi olacaktır: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -Birleştirme ifadesinin kitaba atıfta bulunduğunu fark etmiş olabilirsiniz, ancak `author_id` üzerinden mi yoksa `translator_id` üzerinden mi birleştirdiğimiz net değildir. Yukarıdaki örnekte, Selection `author_id` sütunu üzerinden birleştirir çünkü kaynak tablo ile bir eşleşme bulunmuştur - `author` tablosu. Böyle bir eşleşme olmasaydı ve daha fazla olasılık olsaydı, Nette [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] atardı. +Anahtar olarak `null` belirtilirse, dizi sıfırdan başlayarak sayısal olarak indekslenecektir: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` -`translator_id` sütunu aracılığıyla bir birleştirme yapmak için, birleştirme ifadesinde isteğe bağlı bir parametre sağlayın. +Ayrıca, her satır için ya değerin kendisini ya da bir anahtar-değer çifti döndürecek bir geri çağırmayı parametre olarak iletebilirsiniz. Geri arama yalnızca bir değer döndürürse, anahtar satırın birincil anahtarı olacaktır: ```php -$authors = $explorer->table('author'); -$authors->group('author.id') - ->having('COUNT(:book(translator).id) > 3'); +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'İlk Kitap (Jan Novak)', ...] + +// Geri arama ayrıca anahtar ve değer çifti içeren bir dizi de döndürebilir: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['İlk Kitap' => 'Jan Novak', ...] ``` -Şimdi biraz daha zor birleştirme ifadelerine bir göz atalım. -PHP hakkında bir şeyler yazmış olan tüm yazarları bulmak istiyoruz. Tüm kitapların etiketleri vardır, bu nedenle PHP etiketi ile herhangi bir kitap yazmış olan yazarları seçmeliyiz. +fetchAll(): array .[method] +--------------------------- + +Tüm satırları, anahtarların birincil anahtar değerleri olduğu `ActiveRow` nesnelerinin ilişkisel bir dizisi olarak döndürür. ```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, ...), ...] ``` -Toplu Sorgular .[#toc-aggregate-queries] ----------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Satır sayısını al -| `$table->count("DISTINCT $column")` | Farklı değerlerin sayısını al -| `$table->min($column)` | Minimum değeri al -| `$table->max($column)` | Maksimum değeri al -| `$table->sum($column)` | Tüm değerlerin toplamını alın -| `$table->aggregation("GROUP_CONCAT($column)")` | Herhangi bir toplama işlevini çalıştırın +Parametreleri olmayan `count()` yöntemi, `Selection` nesnesindeki satır sayısını döndürür: -.[caution] -Herhangi bir parametre belirtilmeyen `count()` yöntemi tüm kayıtları seçer ve dizi boyutunu döndürür, bu da çok verimsizdir. Örneğin, sayfalama için satır sayısını hesaplamanız gerekiyorsa, her zaman ilk bağımsız değişkeni belirtin. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternatif +``` +Not: `count()` bir parametre ile birlikte, aşağıda açıklandığı gibi veritabanında COUNT toplama işlevini gerçekleştirir. -Kaçış ve Alıntı .[#toc-escaping-quoting] -======================================== -Veritabanı Gezgini akıllıdır ve parametreleri ve tırnak tanımlayıcılarını sizin için kaçar. Yine de bu temel kurallara uyulması gerekir: +ActiveRow::toArray(): array .[method] +------------------------------------- -- anahtar kelimeler, fonksiyonlar, prosedürler büyük harfle yazılmalıdır -- sütunlar ve tablolar küçük harfle yazılmalıdır -- değişkenleri parametre olarak geçirin, birleştirme yapmayın + `ActiveRow` nesnesini, anahtarların sütun adları ve değerlerin karşılık gelen veriler olduğu bir ilişkisel diziye dönüştürür. ```php -->where('name like ?', 'John'); // WRONG! generates: `isim` `gibi` ? -->where('name LIKE ?', 'John'); // DOĞRU +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray ['id' => 1, 'title' => '...', 'author_id' => ..., ...] olacaktır. +``` + -->where('KEY = ?', $value); // YANLIŞ! KEY bir anahtar kelimedir -->where('key = ?', $value); // DOĞRU. üretir: anahtar` = ? +Birleştirme .[#toc-aggregation] +=============================== -->where('name = ' . $name); // YANLIŞ! sql enjeksiyonu! -->where('name = ?', $name); // DOĞRU + `Selection` sınıfı, toplama işlevlerini (COUNT, SUM, MIN, MAX, AVG, vb.) kolayca gerçekleştirmek için yöntemler sağlar. -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // YANLIŞ! değişkenleri parametre olarak geçirin, birleştirmeyin -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // DOĞRU +.[language-php] +| `count($expr)` | Satır sayısını sayar | +| `min($expr)` | Bir sütundaki minimum değeri döndürür | +| `max($expr)` | Bir sütundaki maksimum değeri döndürür | +| `sum($expr)` | Bir sütundaki değerlerin toplamını verir | +| `aggregation($function)` | `AVG()` veya `GROUP_CONCAT()` gibi herhangi bir toplama işlevine izin verir | + + +count(string $expr): int .[method] +---------------------------------- + +COUNT işleviyle bir SQL sorgusu çalıştırır ve sonucu döndürür. Bu yöntem, kaç satırın belirli bir koşulla eşleştiğini belirlemek için kullanılır: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -.[warning] -Yanlış kullanım güvenlik açıkları oluşturabilir +Not: [count() |#count()] parametresiz olarak sadece `Selection` nesnesindeki satır sayısını döndürür. -Veri Getirme .[#toc-fetching-data] -================================== +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- -| `foreach ($table as $id => $row)` | Sonuçtaki tüm satırlar üzerinde yinele -| `$row = $table->get($id)` | Tablodan $id kimliğine sahip tek bir satır al -| `$row = $table->fetch()` | Sonuçtan bir sonraki satırı al -| `$array = $table->fetchPairs($key, $value)` | Tüm değerleri ilişkisel diziye getir -| `$array = $table->fetchPairs($value)` | Tüm satırları ilişkisel diziye getir -| `count($table)` | Sonuç kümesindeki satır sayısını al + `min()` ve `max()` yöntemleri, belirtilen sütun veya ifadedeki minimum ve maksimum değerleri döndürür: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Belirtilen sütundaki veya ifadedeki değerlerin toplamını döndürür: + +```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): mixed .[method] +----------------------------------------------------------------------------- + +Herhangi bir toplama fonksiyonunun yürütülmesine izin verir. + +```php +// Bir kategorideki ürünlerin ortalama fiyatını hesaplar +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// Ürün etiketlerini tek bir dizede birleştirir +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Kendileri bir toplama ve gruplamadan kaynaklanan sonuçları toplamamız gerekiyorsa (örneğin, gruplanmış satırlar üzerinden `SUM(value)` ), bu ara sonuçlara uygulanacak toplama işlevini ikinci bağımsız değişken olarak belirtiriz: + +```php +// Her kategori için stoktaki ürünlerin toplam fiyatını hesaplar, ardından bu fiyatları toplar +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +Bu örnekte, önce her kategorideki ürünlerin toplam fiyatını hesaplıyoruz (`SUM(price * stock) AS category_total`) ve sonuçları `category_id`'a göre gruplandırıyoruz. Daha sonra bu alt toplamları toplamak için `aggregation('SUM(category_total)', 'SUM')` adresini kullanıyoruz. İkinci bağımsız değişken `'SUM'` ara sonuçlara uygulanacak toplama işlevini belirtir. Ekle, Güncelle ve Sil .[#toc-insert-update-delete] ================================================== -`insert()` yöntemi Traversable nesneleri dizisini kabul eder (örneğin [formları |forms:] döndüren [ArrayHash |utils:arrays#ArrayHash] ): +Nette Database Explorer veri eklemeyi, güncellemeyi ve silmeyi basitleştirir. Bahsedilen tüm yöntemler hata durumunda bir `Nette\Database\DriverException` fırlatır. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Bir tabloya yeni kayıtlar ekler. + +**Tek bir kayıt ekleniyor:** + +Yeni kayıt, anahtarların tablodaki sütun adlarıyla eşleştiği bir ilişkisel dizi veya yinelenebilir nesne ( [formlarda |forms:] kullanılan `ArrayHash` gibi) olarak aktarılır. + +Tablonun tanımlanmış bir birincil anahtarı varsa, yöntem, veritabanı düzeyinde yapılan değişiklikleri (örn. tetikleyiciler, varsayılan sütun değerleri veya otomatik artırma hesaplamaları) yansıtmak için veritabanından yeniden yüklenen bir `ActiveRow` nesnesi döndürür. Bu, veri tutarlılığını sağlar ve nesne her zaman geçerli veritabanı verilerini içerir. Birincil anahtar açıkça tanımlanmamışsa, yöntem girdi verilerini bir dizi olarak döndürür. ```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, eklenen satırın tam verilerini içeren bir ActiveRow örneğidir, +// otomatik olarak oluşturulan kimlik ve tetikleyiciler tarafından yapılan değişiklikler dahil +echo $row->id; // Yeni eklenen kullanıcının kimliğini verir +echo $row->created_at; // Bir tetikleyici tarafından ayarlanmışsa oluşturma zamanını verir ``` -Tabloda birincil anahtar tanımlanmışsa, eklenen satırı içeren bir ActiveRow nesnesi döndürülür. +**Tek seferde birden fazla kayıt ekleme:** -Çoklu ekleme: + `insert()` yöntemi, tek bir SQL sorgusu ile birden fazla kayıt eklemenize olanak tanır. Bu durumda, eklenen satırların sayısını döndürür. ```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 2 olacaktır ``` -Dosyalar veya DateTime nesneleri parametre olarak geçirilebilir: +Parametre olarak veri seçimi içeren bir `Selection` nesnesi de aktarabilirsiniz. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Özel değerlerin eklenmesi:** + +Değerler dosyaları, `DateTime` nesnelerini veya SQL değişmezlerini içerebilir: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // or $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // inserts the file + 'name' => 'John', + 'created_at' => new DateTime, // veritabanı formatına dönüştürür + 'avatar' => fopen('image.jpg', 'rb'), // ikili dosya içeriğini ekler + 'uuid' => $explorer::literal('UUID()'), // UUID() işlevini çağırır ]); ``` -Güncelleme (etkilenen satırların sayısını döndürür): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Bir tablodaki satırları belirtilen filtreye göre günceller. Gerçekte değiştirilen satır sayısını döndürür. + +Güncellenecek sütunlar, anahtarların tablodaki sütun adlarıyla eşleştiği bir ilişkisel dizi veya yinelenebilir nesne ( [formlarda |forms:] kullanılan `ArrayHash` gibi) olarak geçirilir: ```php -$count = $explorer->table('users') - ->where('id', 10) // update() işlevinden önce çağrılmalıdır +$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 ``` -Güncelleme için `+=` a `-=` operatörlerini kullanabiliriz: +Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabilirsiniz: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // 'puanlar' sütununun değerini 1 artırır + 'coins-=' => 1, // 'madeni paralar' sütununun değerini 1 azaltır ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Silme (silinen satırların sayısını döndürür): + +Selection::delete(): int .[method] +---------------------------------- + +Belirtilen filtreye göre bir tablodan satırları siler. Silinen satırların sayısını döndürür. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] + `update()` veya `delete()` adreslerini çağırırken, güncellenecek veya silinecek satırları belirtmek için `where()` adresini kullandığınızdan emin olun. `where()` kullanılmazsa, işlem tüm tablo üzerinde gerçekleştirilecektir! + -İlişkilerle Çalışmak .[#toc-working-with-relationships] -======================================================= +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- + `ActiveRow` nesnesi tarafından temsil edilen bir veritabanı satırındaki verileri günceller. Anahtarların sütun adları olduğu yinelenebilir verileri parametre olarak kabul eder. Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabilirsiniz: -Bir İlişkisi Var .[#toc-has-one-relation] ------------------------------------------ -Bir ilişkiye sahip olmak yaygın bir kullanım durumudur. Kitabın *bir* yazarı vardır. Kitabın *bir* çevirmeni vardır. İlgili satırı almak esas olarak `ref()` yöntemi ile yapılır. İki bağımsız değişken kabul eder: hedef tablo adı ve kaynak birleştirme sütunu. Örneğe bakınız: +Güncelleme gerçekleştirildikten sonra, `ActiveRow` veritabanı düzeyinde yapılan değişiklikleri (örn. tetikleyiciler) yansıtmak için veritabanından otomatik olarak yeniden yüklenir. Yöntem, yalnızca gerçek bir veri değişikliği meydana gelmişse `true` döndürür. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // görüntüleme sayısını artırır +]); +echo $article->views; // Geçerli görüntüleme sayısını verir ``` -Yukarıdaki örnekte, `author` tablosundan ilgili yazar girdisini getiriyoruz, yazar birincil anahtarı `book.author_id` sütunu tarafından aranır. Ref() metodu ActiveRow örneğini döndürür veya uygun bir girdi yoksa null döndürür. Dönen satır bir ActiveRow örneğidir, bu nedenle kitap girişiyle aynı şekilde çalışabiliriz. +Bu yöntem, veritabanında yalnızca belirli bir satırı günceller. Birden fazla satırın toplu güncellemeleri için [Selection::update() |#Selection::update()] yöntemini kullanın. + + +ActiveRow::delete() .[method] +----------------------------- + +Veritabanından `ActiveRow` nesnesi tarafından temsil edilen bir satırı siler. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // ID 1 olan kitabı siler +``` -// veya doğrudan -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; +Bu yöntem, veritabanındaki yalnızca belirli bir satırı siler. Birden fazla satırı toplu olarak silmek için [Selection::delete() |#Selection::delete()] yöntemini kullanın. + + +Tablolar Arasındaki İlişkiler .[#toc-relationships-between-tables] +================================================================== + +İlişkisel veritabanlarında veriler birden fazla tabloya bölünür ve yabancı anahtarlar aracılığıyla birbirine bağlanır. Nette Database Explorer, JOIN sorguları yazmadan veya herhangi bir yapılandırma veya varlık oluşturma gerektirmeden bu ilişkilerle çalışmak için devrim niteliğinde bir yol sunar. + +Gösterim için **example veritabanını** kullanacağız[(GitHub'da mevcuttur |https://github.com/nette-examples/books]). Veritabanı aşağıdaki tabloları içerir: + +- `author` - yazarlar ve çevirmenler ( `id`, `name`, `web`, `born` sütunları) +- `book` - kitaplar ( `id`, `author_id`, `translator_id`, `title`, `sequel_id` sütunları) +- `tag` - etiketler ( `id`, `name` sütunları) +- `book_tag` - kitaplar ve etiketler arasındaki bağlantı tablosu ( `book_id`, `tag_id` sütunları) + +[* db-schema-1-.webp *] *** Veritabanı yapısı .<> + +Bu kitap veritabanı örneğinde, çeşitli ilişki türleri buluyoruz (gerçeğe kıyasla basitleştirilmiş): + +- Birden çoğa (1:N)** - Her kitabın **bir** yazarı vardır; bir yazar **birden fazla** kitap yazabilir. +- Sıfırdan çoğa (0:N)** - Bir kitabın **bir çevirmeni** olabilir; bir çevirmen **birden fazla** kitabı çevirebilir. +- Sıfırdan bire (0:1)** - Bir kitabın **bir devam kitabı** olabilir. +- Çoktan çoğa (M:N)** - Bir kitap **birden fazla** etikete sahip olabilir ve bir etiket **birden fazla** kitaba atanabilir. + +Bu ilişkilerde her zaman bir **ana tablo** ve bir **çocuk tablo** vardır. Örneğin, yazarlar ve kitaplar arasındaki ilişkide, `author` tablosu üst tablodur ve `book` tablosu alt tablodur - bunu her zaman bir yazara "ait" bir kitap olarak düşünebilirsiniz. Bu durum veritabanı yapısına da yansır: `book` alt tablosu, `author` üst tablosuna referans veren `author_id` yabancı anahtarını içerir. + +Kitapları yazarlarının adlarıyla birlikte görüntülemek istiyorsak, iki seçeneğimiz vardır. Ya bir JOIN ile tek bir SQL sorgusu kullanarak verileri alırız: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Ya da verileri iki adımda alırız - önce kitapları, sonra yazarlarını - ve bunları PHP'de bir araya getiririz: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Kitapta ayrıca bir çevirmen var, bu nedenle çevirmen adını almak oldukça kolay. +İkinci yaklaşım şaşırtıcı bir şekilde **daha verimlidir**. Veriler yalnızca bir kez getirilir ve önbellekte daha iyi kullanılabilir. Nette Database Explorer tam olarak bu şekilde çalışır - her şeyi kaputun altında halleder ve size temiz bir API sağlar: + ```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, 'author' tablosundan bir kayıttır + echo 'translated by: ' . $book->translator?->name; +} ``` -Tüm bunlar iyi, ancak biraz zahmetli, öyle değil mi? Veritabanı Gezgini zaten yabancı anahtar tanımlarını içeriyor, o halde neden bunları otomatik olarak kullanmayalım? Hadi bunu yapalım! -Eğer var olmayan bir özelliği çağırırsak, ActiveRow çağırılan özellik adını 'has one' ilişkisi olarak çözümlemeye çalışır. Bu özelliği almak, ref() metodunu sadece bir argümanla çağırmakla aynıdır. Tek argümanı **key** olarak adlandıracağız. Anahtar, belirli bir yabancı anahtar ilişkisine çözümlenecektir. İletilen anahtar satır sütunlarıyla eşleştirilir ve eşleşirse, eşleşen sütunda tanımlanan yabancı anahtar ilgili hedef tablodan veri almak için kullanılır. Örneğe bakınız: +Ana Tabloya Erişim .[#toc-accessing-the-parent-table] +----------------------------------------------------- + +Ana tabloya erişim basittir. Bunlar *bir kitabın bir yazarı vardır* veya *bir kitabın bir çevirmeni olabilir* gibi ilişkilerdir. İlgili kayda `ActiveRow` nesne özelliği aracılığıyla erişilebilir - özellik adı, `id` son eki olmadan yabancı anahtarın sütun adıyla eşleşir: ```php -$book->author->name; -// same as -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // 'author_id' sütunu aracılığıyla yazarı bulur +echo $book->translator?->name; // 'translator_id' sütunu aracılığıyla çevirmeni bulur ``` -ActiveRow örneğinin yazar sütunu yoktur. Tüm kitap sütunları *key* ile eşleşme için aranır. Bu durumda eşleştirme, sütun adının anahtarı içermesi gerektiği anlamına gelir. Yukarıdaki örnekte, `author_id` sütunu 'author' dizesini içerir ve bu nedenle 'author' anahtarıyla eşleştirilir. Kitap çevirmenini almak istiyorsanız, anahtar olarak örneğin 'translator' kullanabilirsiniz, çünkü 'translator' anahtarı `translator_id` sütunuyla eşleşecektir. Anahtar eşleştirme mantığı hakkında daha fazla bilgiyi [Joining expressions |#joining-key] bölümünde bulabilirsiniz. +Explorer, `$book->author` özelliğine erişirken, `book` tablosunda `author` dizesini (yani, `author_id`) içeren bir sütun arar. Bu sütundaki değere bağlı olarak, `author` tablosundan ilgili kaydı alır ve bunu bir `ActiveRow` nesnesi olarak döndürür. Benzer şekilde `$book->translator`, `translator_id` sütununu kullanır. `translator_id` sütunu `null` içerebileceğinden `?->` operatörü kullanılır. + +Alternatif bir yaklaşım, iki bağımsız değişken (hedef tablonun adı ve bağlantı sütunu) kabul eden ve bir `ActiveRow` örneği veya `null` döndüren `ref()` yöntemi tarafından sağlanır: ```php -echo $book->title . ': '; -echo $book->author->name; -if ($book->translator) { - echo ' (translated by ' . $book->translator->name . ')'; -} +echo $book->ref('author', 'author_id')->name; // yazara bağlantı +echo $book->ref('author', 'translator_id')->name; // çevirmene bağlantı ``` -Birden fazla kitap getirmek istiyorsanız, aynı yaklaşımı kullanmalısınız. Nette Database Explorer, getirilen tüm kitaplar için yazarları ve çevirmenleri bir kerede getirecektir. + `ref()` yöntemi, özellik tabanlı erişimin kullanılamadığı durumlarda, örneğin tabloda özellik ile aynı ada sahip bir sütun varsa kullanışlıdır (`author`). Diğer durumlarda, daha iyi okunabilirlik için özellik tabanlı erişim kullanılması önerilir. + +Explorer veritabanı sorgularını otomatik olarak optimize eder. Kitaplar arasında yineleme yaparken ve ilgili kayıtlarına (yazarlar, çevirmenler) erişirken, Explorer her kitap için ayrı ayrı bir sorgu oluşturmaz. Bunun yerine, her bir ilişki türü için yalnızca **bir SELECT sorgusu** çalıştırarak veritabanı yükünü önemli ölçüde azaltır. Örneğin: ```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; } ``` -Kod yalnızca bu 3 sorguyu çalıştıracaktır: +Bu kod yalnızca üç optimize edilmiş veritabanı sorgusu yürütecektir: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Bağlantı sütununu tanımlama mantığı [Conventions |api:Nette\Database\Conventions] uygulaması tarafından tanımlanır. Yabancı anahtarları analiz eden ve mevcut tablo ilişkileriyle sorunsuz bir şekilde çalışmanıza olanak tanıyan [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions]'ı kullanmanızı öneririz. -Birçok İlişkisi Var .[#toc-has-many-relation] ---------------------------------------------- -'Has many' ilişkisi 'has one' ilişkisinin tersine çevrilmiş halidir. Yazar *çok* kitap yazmıştır. Yazar *çok* kitap çevirmiştir. Gördüğünüz gibi, bu tür bir ilişki biraz daha zordur çünkü ilişki 'adlandırılmıştır' ('yazılmıştır', 'çevrilmiştir'). ActiveRow örneği, ilgili girdilerin dizisini döndürecek olan `related()` yöntemine sahiptir. Girişler de ActiveRow örnekleridir. Aşağıdaki örneğe bakınız: +Çocuk Tablosuna Erişim .[#toc-accessing-the-child-table] +-------------------------------------------------------- + +Alt tabloya erişim ters yönde çalışır. Şimdi *bu yazar hangi kitapları yazdı* veya *bu çevirmen hangi kitapları çevirdi* diye soruyoruz. Bu tür bir sorgu için, ilgili kayıtları içeren bir `Selection` nesnesi döndüren `related()` yöntemini kullanırız. İşte bir örnek: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' has written:'; +$author = $explorer->table('author')->get(1); +// Yazar tarafından yazılan tüm kitapları çıkarır foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'and translated:'; +// Yazar tarafından çevrilen tüm kitapları çıkarır foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Yöntem `related()` yöntemi, iki bağımsız değişken olarak veya nokta ile birleştirilmiş bir bağımsız değişken olarak aktarılan tam birleştirme açıklamasını kabul eder. İlk bağımsız değişken hedef tablo, ikincisi ise hedef sütundur. + `related()` yöntemi, ilişki açıklamasını nokta gösterimini kullanarak tek bir bağımsız değişken olarak veya iki ayrı bağımsız değişken olarak kabul eder: ```php -$author->related('book.translator_id'); -// ile aynı -$author->related('book', 'translator_id'); +$author->related('book.translator_id'); // tek argüman +$author->related('book', 'translator_id'); // iki argüman ``` -Yabancı anahtarlara dayalı Nette Database Explorer buluşsal yöntemlerini kullanabilir ve yalnızca **key** bağımsız değişkenini sağlayabilirsiniz. Anahtar, geçerli tabloya işaret eden tüm yabancı anahtarlarla eşleştirilecektir (`author` tablo). Bir eşleşme varsa, Nette Database Explorer bu yabancı anahtarı kullanacaktır, aksi takdirde [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] veya [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] atacaktır. Anahtar eşleştirme mantığı hakkında daha fazla bilgiyi [Joining expressions |#joining-key] bölümünde bulabilirsiniz. +Explorer, üst tablonun adına göre doğru bağlantı sütununu otomatik olarak algılayabilir. Bu durumda, kaynak tablonun adı `author` olduğu için `book.author_id` sütunu üzerinden bağlantı verir: -Elbette, getirilen tüm yazarlar için ilgili yöntemleri çağırabilirsiniz, Nette Database Explorer uygun kitapları bir kerede tekrar getirecektir. +```php +$author->related('book'); // book.author_id kullanır +``` + +Birden fazla olası bağlantı varsa, Explorer bir [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] istisnası atacaktır. + +Elbette, bir döngü içinde birden fazla kayıt arasında yineleme yaparken `related()` yöntemini de kullanabiliriz ve Explorer bu durumda da sorguları otomatik olarak optimize edecektir: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' has written:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -Yukarıdaki örnek yalnızca iki sorgu çalıştıracaktır: +Bu kod yalnızca iki verimli SQL sorgusu oluşturur: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ids of fetched authors +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors +``` + + +Çoktan Çoka İlişki .[#toc-many-to-many-relationship] +---------------------------------------------------- + +Çoktan çoka (M:N) ilişki için bir **birleşim tablosu** (bizim durumumuzda `book_tag`) gereklidir. Bu tablo iki yabancı anahtar sütunu içerir (`book_id`, `tag_id`). Her sütun, bağlı tablolardan birinin birincil anahtarına referans verir. İlgili verileri almak için, önce `related('book_tag')` adresini kullanarak bağlantı tablosundan kayıtları getiriyoruz ve ardından hedef verilere devam ediyoruz: + +```php +$book = $explorer->table('book')->get(1); +// Kitaba atanan etiketlerin adlarını çıktı olarak verir +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // bağlantı tablosu aracılığıyla etiket adını getirir +} + +$tag = $explorer->table('tag')->get(1); +// Ters yön: bu etikete sahip kitapların başlıklarını çıkarır +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // kitap başlığını getirir +} +``` + +Explorer SQL sorgularını tekrar verimli bir forma optimize eder: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag ``` -Explorer'ı Manuel Olarak Oluşturma .[#toc-creating-explorer-manually] -===================================================================== +İlgili Tablolar Üzerinden Sorgulama .[#toc-querying-through-related-tables] +--------------------------------------------------------------------------- -Uygulama yapılandırması kullanılarak bir veritabanı bağlantısı oluşturulabilir. Bu gibi durumlarda bir `Nette\Database\Explorer` hizmeti oluşturulur ve DI konteyneri kullanılarak bir bağımlılık olarak aktarılabilir. + `where()`, `select()`, `order()` ve `group()` yöntemlerinde, diğer tablolardan sütunlara erişmek için özel gösterimler kullanabilirsiniz. Explorer gerekli JOIN'leri otomatik olarak oluşturur. -Ancak, Nette Database Explorer bağımsız bir araç olarak kullanılıyorsa, `Nette\Database\Explorer` nesnesinin bir örneğinin manuel olarak oluşturulması gerekir. +Ana tablonun perspektifinden bakıldığında 1:N ilişkiler için **Nokta gösterimi** (`parent_table.column`) kullanılır: ```php -// $storage Nette\Caching\Storage uygular: -$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); +$books = $explorer->table('book'); + +// Yazarlarının adı 'Jon' ile başlayan kitapları bulur +$books->where('author.name LIKE ?', 'Jon%'); + +// Kitapları yazar adına göre azalan şekilde sıralar +$books->order('author.name DESC'); + +// Kitap başlığı ve yazar adı çıktıları +$books->select('book.title, author.name'); +``` + +Üst tablo açısından 1:N ilişkiler için **Kolon gösterimi** kullanılır: + +```php +$authors = $explorer->table('author'); + +// Başlığında 'PHP' geçen bir kitap yazan yazarları bulur +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Her yazar için kitap sayısını sayar +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Yukarıdaki iki nokta üst üste gösterimli örnekte (`:book.title`), yabancı anahtar sütunu açıkça belirtilmemiştir. Explorer, üst tablo adına göre doğru sütunu otomatik olarak algılar. Bu durumda, kaynak tablonun adı `author` olduğu için `book.author_id` sütunu üzerinden birleşir. Birden fazla olası bağlantı varsa, Explorer [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] istisnasını atar. + +Bağlantı sütunu parantez içinde açıkça belirtilebilir: + +```php +// Başlığında 'PHP' geçen bir kitabı çeviren yazarları bulur +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Birden fazla tablodaki verilere erişmek için gösterimler zincirlenebilir: + +```php +// 'PHP' ile etiketlenmiş kitapların yazarlarını bulur +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +JOIN için Genişletme Koşulları .[#toc-extending-conditions-for-join] +-------------------------------------------------------------------- + + `joinWhere()` yöntemi, SQL'deki tablo birleştirmelerine `ON` anahtar sözcüğünden sonra ek koşullar ekler. + +Örneğin, belirli bir çevirmen tarafından çevrilmiş kitapları bulmak istediğimizi varsayalım: + +```php +// 'David' adında bir çevirmen tarafından çevrilmiş kitaplar bulur +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + + `joinWhere()` koşulunda, `where()` yönteminde olduğu gibi aynı yapıları kullanabilirsiniz - operatörler, yer tutucular, değer dizileri veya SQL ifadeleri. + +Birden fazla JOIN içeren daha karmaşık sorgular için tablo takma adları tanımlanabilir: + +```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 (`kitap_yazarı`.`doğum` < 1950) +``` + + `where()` yöntemi `WHERE` cümlesine koşullar eklerken, `joinWhere()` yönteminin tablo birleştirmeleri sırasında `ON` cümlesindeki koşulları genişlettiğini unutmayın. + + +Elle Gezgin Oluşturma .[#toc-manually-creating-explorer] +======================================================== + +Nette DI konteynerini kullanmıyorsanız, `Nette\Database\Explorer` örneğini manuel olarak oluşturabilirsiniz: + +```php +use Nette\Database; + +// $storage, Nette\Caching\Storage'ı uygular, örn: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// veritabanı bağlantısı +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// veritabanı yapısının yansımasını yönetir +$structure = new Database\Structure($connection, $storage); +// tablo adlarını, sütunları ve yabancı anahtarları eşlemek için kuralları tanımlar +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/tr/security.texy b/database/tr/security.texy new file mode 100644 index 0000000000..e9ca2ca139 --- /dev/null +++ b/database/tr/security.texy @@ -0,0 +1,145 @@ +Güvenlik Riskleri +***************** + +.[perex] +Veritabanları genellikle hassas veriler içerir ve tehlikeli işlemlere izin verir. Nette Veritabanı bir dizi güvenlik özelliği sağlar. Ancak, güvenli ve güvenli olmayan API'ler arasındaki farkı anlamak çok önemlidir. + + +SQL Enjeksiyonu .[#toc-sql-injection] +===================================== + +SQL enjeksiyonu, veritabanlarıyla çalışırken karşılaşılan en ciddi güvenlik riskidir. Kontrol edilmemiş kullanıcı girdisi bir SQL sorgusunun parçası haline geldiğinde ortaya çıkar. Bir saldırgan kendi SQL komutlarını enjekte ederek veritabanındaki verileri elde edebilir veya değiştirebilir. + +```php +// ❌ GÜVENLİ OLMAYAN KOD - SQL enjeksiyonuna karşı savunmasız +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Saldırgan şöyle bir şey girebilir: ' VEYA '1'='1 +// Ortaya çıkan sorgu şöyle olacaktır: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Bu, tüm kullanıcıları döndürür! +``` + +Aynı durum Veritabanı Gezgini için de geçerlidir: + +```php +// ❌ GÜVENLİ OLMAYAN KOD +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Güvenli Parametrelendirilmiş Sorgular .[#toc-safe-parameterized-queries] +======================================================================== + +SQL sorgularına değer eklemenin güvenli yolu parametrelendirilmiş sorgulardır. Nette Veritabanı bunları kullanmak için çeşitli yollar sunar. + + +Yer Tutucu Soru İşaretleri .[#toc-placeholder-question-marks] +------------------------------------------------------------- + +En basit yöntem yer tutucu soru işaretleri kullanmaktır: + +```php +// ✅ Güvenli parametrelendirilmiş sorgular +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Explorer'da güvenli durum +$table->where('name = ?', $_GET['name']); +``` + +Aynı durum, Veritabanı Gezgini'nde yer tutucu soru işaretleri ve parametreler içeren ifadelerin eklenmesine izin veren diğer tüm yöntemler için de geçerlidir. + +.[warning] +Değerler skaler tipte olmalıdır (`string`, `int`, `float`, `bool`) veya `null`, `$_GET['name']` bir dizi ise, Nette Database tüm elemanlarını SQL sorgusuna dahil edecektir, bu da istenmeyen bir durum olabilir. + + +Değer Dizileri .[#toc-value-arrays] +----------------------------------- + + `INSERT`, `UPDATE` veya `WHERE` cümleleri için değer dizileri kullanabiliriz: + +```php +// ✅ Güvenli INSERT +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// ✅ Güvenli Güncelleme +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +Nette Veritabanı, parametrelendirilmiş sorgulardan geçirilen tüm değerleri otomatik olarak kaçar. Ancak, parametrelerin doğru veri türüne sahip olduğundan emin olmalıyız. + + +Dizi Anahtarları Güvenli Bir API Değildir .[#toc-array-keys-are-not-a-safe-api] +=============================================================================== + +Dizilerdeki değerler güvenliyken, aynı şey anahtarlar için söylenemez: + +```php +// ❌ GÜVENLİ OLMAYAN KOD - anahtarlar SQL enjeksiyonu içerebilir +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + + `INSERT` ve `UPDATE` komutları için bu kritik bir güvenlik açığıdır - bir saldırgan veritabanındaki herhangi bir sütunu ekleyebilir veya değiştirebilir. Örneğin, `is_admin = 1` adresini ayarlayabilir veya hassas sütunlara rastgele veri ekleyebilirler. + + `WHERE` koşullarında, bu daha da tehlikelidir çünkü veritabanı hakkında kademeli olarak bilgi almak için bir teknik olan **SQL numaralandırmasına** izin verir. Bir saldırgan `$_GET` adresine bu şekilde enjekte ederek çalışan maaşlarını keşfetmeye çalışabilir: + +```php +$_GET = ['salary >', 100000]; // maaş aralıklarını belirlemeye başlar +``` + +Ancak asıl sorun, `WHERE` koşullarının anahtarlardaki SQL ifadelerini desteklemesidir: + +```php +// Anahtarlarda operatörlerin meşru kullanımı +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// ❌ GÜVENLİ DEĞİL: saldırgan kendi SQL'ini enjekte edebilir +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // saldırganın yönetici maaşlarını elde etmesini sağlar +``` + +Bu bir kez daha **SQL enjeksiyonudur**. + + +Sütunları Beyaz Listeye Alma .[#toc-whitelisting-columns] +--------------------------------------------------------- + +Kullanıcıların sütun seçmesine izin vermek istiyorsanız, her zaman bir beyaz liste kullanın: + +```php +// ✅ Güvenli işleme - sadece izin verilen sütunlar +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Dinamik Tanımlayıcılar .[#toc-dynamic-identifiers] +================================================== + +Dinamik tablo ve sütun adları için `?name` yer tutucusunu kullanın: + +```php +// ✅ Güvenilir tanımlayıcıların güvenli kullanımı +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// ❌ GÜVENLİ DEĞİL - asla kullanıcı girişi kullanmayın +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + + `?name` sembolü yalnızca uygulama kodunda tanımlanan güvenilir değerler için kullanılmalıdır. Kullanıcı tarafından sağlanan değerler için yine bir beyaz liste kullanın. diff --git a/database/uk/@left-menu.texy b/database/uk/@left-menu.texy index ab9dc184ff..75036dbf35 100644 --- a/database/uk/@left-menu.texy +++ b/database/uk/@left-menu.texy @@ -4,3 +4,4 @@ - [Explorer |Explorer] - [Рефлексія |Reflection] - [Налаштування |configuration] +- [Ризики безпеки |security] diff --git a/database/uk/explorer.texy b/database/uk/explorer.texy index c95cc9b0c8..66a3195d17 100644 --- a/database/uk/explorer.texy +++ b/database/uk/explorer.texy @@ -1,550 +1,929 @@ -Провідник баз даних -******************* +Провідник бази даних +********************
-Nette Database Explorer значно спрощує отримання даних з бази даних без написання SQL-запитів. +Nette Database Explorer - це потужний шар, який значно спрощує отримання даних з бази даних без необхідності написання SQL-запитів. -- використовує ефективні запити -- дані не передаються без необхідності -- вирізняється елегантним синтаксисом +- Робота з даними стає природною та зрозумілою +- Генерує оптимізовані SQL-запити, які витягують лише необхідні дані +- Забезпечує легкий доступ до пов'язаних даних без необхідності написання JOIN-запитів +- Працює відразу, без будь-якого налаштування або створення сутностей
-Щоб використовувати Database Explorer, почніть з таблиці - викличте `table()` на об'єкті [api:Nette\Database\Explorer]. Найпростіше отримати екземпляр контекстного об'єкта [описано тут |core#Connection-and-Configuration], або, у випадку, коли Nette Database Explorer використовується як окремий інструмент, його можна [створити вручну |#Creating-Explorer-Manually]. +Nette Database Explorer - це розширення низькорівневого шару [Nette Database Core |core], яке додає зручний об'єктно-орієнтований підхід до управління базами даних. + +Робота з Explorer починається з виклику методу `table()` на об'єкті [api:Nette\Database\Explorer] (як його отримати, [описано тут |core#Connection and Configuration]): ```php -$books = $explorer->table('book'); // ім'я таблиці в бд - 'book' +$books = $explorer->table('book'); // 'book' - це ім'я таблиці ``` -Виклик повертає екземпляр об'єкта [Selection |api:Nette\Database\Table\Selection], який можна ітерувати для отримання всіх книг. Кожен елемент (рядок) представлений екземпляром [ActiveRow |api:Nette\Database\Table\ActiveRow] з даними, відображеними в його властивостях: +Метод повертає об'єкт [Selection |api:Nette\Database\Table\Selection], який представляє собою SQL-запит. До цього об'єкта можна підключити додаткові методи для фільтрації та сортування результатів. Запит збирається і виконується тільки при запиті даних, наприклад, при ітерації за допомогою `foreach`. Кожен рядок представлений об'єктом [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { - echo $book->title; - echo $book->author_id; + echo $book->title; // виводить стовпець 'title' + echo $book->author_id; // виводить колонку 'author_id' } ``` -Отримання тільки одного конкретного ряду здійснюється методом `get()`, який безпосередньо повертає екземпляр ActiveRow. +Explorer значно спрощує роботу з [зв'язками таблиць |#Vazby mezi tabulkami]. Наступний приклад показує, як легко ми можемо вивести дані з пов'язаних таблиць (книги та їх автори). Зверніть увагу, що ніяких JOIN-запитів писати не потрібно - Nette генерує їх за нас: ```php -$book = $explorer->table('book')->get(2); // повертає книгу з ідентифікатором 2 -echo $book->title; -echo $book->author_id; +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // створює JOIN з таблицею 'author' +} ``` -Давайте розглянемо поширений випадок використання. Вам потрібно отримати книги та їхніх авторів. Це звичайне відношення 1:N. Часто використовуваним рішенням є отримання даних за допомогою одного SQL-запиту з об'єднанням таблиць. Друга можливість - отримати дані окремо, виконати один запит для отримання книг, а потім отримати автора для кожної книги іншим запитом (наприклад, у циклі foreach). Це можна легко оптимізувати для виконання лише двох запитів, один для книг, а інший для потрібних авторів - і саме так це робить Nette Database Explorer. +Nette Database Explorer оптимізує запити для досягнення максимальної ефективності. У наведеному вище прикладі виконується всього два запити SELECT, незалежно від того, чи обробляємо ми 10 або 10 000 книг. -У наведених нижче прикладах ми працюватимемо зі схемою бази даних, показаною на малюнку. Є зв'язки OneHasMany (1:N) (автор книжки `author_id` і можливий перекладач `translator_id`, який може бути `null`) і зв'язки ManyHasMany (M:N) між книжкою та її тегами. +Крім того, Explorer відстежує, які стовпці використовуються в коді, і витягує з бази даних тільки їх, що ще більше знижує навантаження. Ця поведінка повністю автоматична і адаптивна. Якщо згодом ви зміните код, щоб використовувати додаткові стовпці, Explorer автоматично скоригує запити. Вам не потрібно нічого налаштовувати або думати про те, які стовпці будуть потрібні - надайте це Nette. -[Приклад, включно зі схемою, можна знайти на GitHub |https://github.com/nette-examples/books]. -[* db-schema-1-.webp *] *** Структура бази даних, що використовується в прикладах .<> +Фільтрація та сортування .[#toc-filtering-and-sorting] +====================================================== -Наступний код перераховує ім'я автора для кожної книги та всі її теги. Ми [обговоримо нижче |#Working-with-Relationships], як це працює всередині. +Клас `Selection` надає методи для фільтрації та сортування даних. -```php -$books = $explorer->table('book'); +.[language-php] +| `where($condition, ...$params)` | Додає умову WHERE. Кілька умов об'єднуються за допомогою AND | +| `whereOr(array $conditions)` | Додає групу умов WHERE, об'єднаних за допомогою OR | +| `wherePrimary($value)` | Додає умову WHERE на основі первинного ключа | +| `order($columns, ...$params)` | Встановлює сортування за допомогою ORDER BY | +| `select($columns, ...$params)` | Вказує, які стовпці слід витягти | +| `limit($limit, $offset = null)` | Обмежує кількість рядків (LIMIT) і опціонально встановлює OFFSET | +| `page($page, $itemsPerPage, &$total = null)` | Встановлює пагінацію | +| `group($columns, ...$params)` | Групує рядки (GROUP BY) | +| `having($condition, ...$params)`| Додає умову HAVING для фільтрації згрупованих рядків | -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author - рядок із таблиці 'author' +Методи можуть бути об'єднані в ланцюжок (так званий [fluent-інтерфейс |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 - рядок із таблиці 'tag' - } -} -``` +Ці методи також дозволяють використовувати спеціальні позначення для доступу до [даних з пов'язаних таблиць |#Dotazování přes související tabulky]. -Ви будете задоволені тим, наскільки ефективно працює шар бази даних. Наведений вище приклад робить постійну кількість запитів, які виглядають наступним чином: -```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)) -``` +Екранування та ідентифікатори .[#toc-escaping-and-identifiers] +-------------------------------------------------------------- -Якщо ви використовуєте [кеш |caching:] (за замовчуванням увімкнено), жодні стовпці не будуть запитуватися без потреби. Після першого запиту в кеші будуть збережені імена використаних стовпців, і Nette Database Explorer буде виконувати запити тільки з потрібними стовпцями: +Методи автоматично екранують параметри і беруть в лапки ідентифікатори (імена таблиць і стовпців), запобігаючи SQL-ін'єкції. Щоб забезпечити правильну роботу, необхідно дотримуватися кількох правил: -```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)) +- Записуйте ключові слова, імена функцій, процедур і т. д. в **верхньому регістрі**. +- Імена стовпців і таблиць пишіть в **нижньому регістрі**. +- Завжди передавайте рядки за допомогою **параметрів**. + +```php +where('name = ' . $name); // **DISASTER**: вразливість до SQL-ін'єкцій +where('name LIKE "%search%"'); // **WRONG**: ускладнює автоматичне цитування +where('name LIKE ?', '%search%'); // **КОРЕКТНО**: значення передається як параметр + +where('name like ?', $name); // **WRONG**: генерує: `name` `like` ? +where('name LIKE ?', $name); // **CORRECT**: генерує: `name` LIKE ? +where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`name`) = ? ``` -Вибірки .[#toc-selections] -========================== +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- -Дивіться можливості фільтрації та обмеження рядків [api:Nette\Database\Table\Selection]: +Фільтрує результати за допомогою умов WHERE. Його сильною стороною є інтелектуальна обробка різних типів значень і автоматичний вибір операторів SQL. -.[language-php] -| `$table->where($where[, $param[, ...]])` | Встановлюємо WHERE, використовуючи AND як клей, якщо задано дві або більше умов -| `$table->whereOr($where)` | Встановлюємо WHERE, використовуючи OR як зв'язку, якщо задано дві або більше умов -| `$table->order($columns)` | Встановлюємо ORDER BY, наприклад, за допомогою виразу `('column DESC, id DESC')`. -| `$table->select($columns)` | Встановлюємо витягнуті стовпці, наприклад, за допомогою виразу `('col, MD5(col) AS hash')`. -| `$table->limit($limit[, $offset])` | Встановлюємо LIMIT та OFFSET -| `$table->page($page, $itemsPerPage[, &$lastPage])` | Вмикаємо пагінацію -| `$table->group($columns)` | Встановлюємо GROUP BY -| `$table->having($having)` | Встановлюємо HAVING +Базове використання: -Ми можемо використовувати так званий вільний [інтерфейс |nette:introduction-to-object-oriented-programming#fluent-interfaces], наприклад `$table->where(...)->order(...)->limit(...)`. Кілька умов `where` або `whereOr` зв'язуються оператором `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' +``` +Завдяки автоматичному визначенню відповідних операторів вам не потрібно розбиратися з особливими випадками - Nette зробить це за вас: -where() .[#toc-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) +// Заповнювач ? може використовуватися без оператора: +$table->where('id ?', 1); // WHERE `id` = 1 +``` -Nette Database Explorer може автоматично додавати необхідні оператори для переданих значень: +Метод також коректно обробляє негативні умови і порожні масиви: -.[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 -- не знаходить нічого +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- знаходить все +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- знаходить все +// $table->where('NOT id ?', $ids); // УВАГА: Цей синтаксис не підтримується +``` -Ви можете вказати заповнювач навіть без оператора column. Ці виклики однакові. +Ви також можете передати результат іншого запиту до таблиці як параметр, створивши підзапит: ```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')); ``` -Ця функція дозволяє генерувати правильний оператор на основі значення: +Умови також можна передати у вигляді масиву, об'єднавши елементи за допомогою 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 коректно обробляє і негативні умови, працює і для порожніх масивів: +У масиві можна використовувати пари ключ-значення, і Nette знову автоматично вибере потрібні оператори: ```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], +]); +``` + +Ми також можемо змішувати SQL-вирази із заповнювачами і декількома параметрами. Це корисно для складних умов з точно визначеними операторами: -// це призведе до виключення, цей синтаксис не підтримується -$table->where('NOT id ?', $ids); +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // два параметри передаються у вигляді масиву +]); ``` +Кілька викликів `where()` автоматично об'єднують умови за допомогою AND. -whereOr() .[#toc-whereor] -------------------------- -Приклад використання без параметрів: +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Аналогічно `where()`, але об'єднує умови за допомогою 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, ]); ``` -Ми використовуємо параметри. Якщо ви не вкажете оператор, Nette Database Explorer автоматично додасть відповідний оператор: +Можна використовувати і більш складні вирази: ```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, ]); ``` -Ключ може містити вираз, що містить підстановні знаки питання, а потім передавати параметри в значенні: + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Додає умову для первинного ключа таблиці: ```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]); +``` + +Якщо таблиця має складений первинний ключ (наприклад, `foo_id`, `bar_id`), ми передаємо його у вигляді масиву: + +```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() .[#toc-order] ---------------------- +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- -Приклади використання: +Вказує порядок, в якому повертаються рядки. Ви можете сортувати по одному або декільком стовпцям, за зростанням або спаданням, або за користувацьким виразом: ```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() .[#toc-select] ------------------------ +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- -Приклади використання: +Вказує стовпці, які будуть повернуті з бази даних. За замовчуванням Nette Database Explorer повертає тільки ті стовпці, які дійсно використовуються в коді. Використовуйте метод `select()`, якщо вам потрібно отримати конкретні вирази: ```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) +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); ``` +Псевдоніми, визначені за допомогою `AS`, стають доступні як властивості об'єкта `ActiveRow`: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // доступ до псевдоніму +} +``` -limit() .[#toc-limit] ---------------------- -Приклади використання: +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Обмежує кількість рядків, що повертаються (LIMIT) і опціонально задає зміщення: ```php -$table->limit(1); // LIMIT 1 -$table->limit(1, 10); // LIMIT 1 OFFSET 10 +$table->limit(10); // LIMIT 10 (повертає перші 10 рядків) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` +Для пагінації доцільніше використовувати метод `page()`. -page() .[#toc-page] -------------------- -Альтернативний спосіб встановлення межі (limit) і зміщення (offset): +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Спрощує пагінацію результатів. Приймає номер сторінки (починаючи з 1) і кількість елементів на сторінці. Як опцію можна передати посилання на змінну, в якій буде зберігатися загальна кількість сторінок: ```php -$page = 5; -$itemsPerPage = 10; -$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40 +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Total pages: $numOfPages"; ``` -Отримання номера останньої сторінки, переданого у змінну `$lastPage`: + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Групує рядки за вказаними стовпцями (GROUP BY). Зазвичай використовується в поєднанні з агрегатними функціями: ```php -$table->page($page, $itemsPerPage, $lastPage); +// Підраховує кількість товарів у кожній категорії +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); ``` -group() .[#toc-group] ---------------------- +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- -Приклади використання: +Задає умову для фільтрації згрупованих рядків (HAVING). Може використовуватися в поєднанні з методом `group()` і агрегатними функціями: ```php -$table->group('field1'); // GROUP BY `field1` -$table->group('field1, field2'); // GROUP BY `field1`, `field2` +// Знаходить категорії з більш ніж 100 товарами +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); ``` -having() .[#toc-having] ------------------------ +Читання даних .[#toc-reading-data] +================================== + +Для читання даних з бази даних існує кілька корисних методів: + +.[language-php] +| `foreach ($table as $key => $row)` | Ітерація по всіх рядках, `$key` - значення первинного ключа, `$row` - об'єкт ActiveRow | +| `$row = $table->get($key)` | Повертає один рядок за первинним ключем | +| `$row = $table->fetch()` | Повертає поточний рядок і переводить покажчик на наступний | +| `$array = $table->fetchPairs()` | Створює асоціативний масив з результатів | +| `$array = $table->fetchAll()` | Повертає всі рядки у вигляді масиву | +| `count($table)` | Повертає кількість рядків в об'єкті Selection | + +Об'єкт [ActiveRow |api:Nette\Database\Table\ActiveRow] доступний тільки для читання. Це означає, що ви не можете змінювати значення його властивостей. Це обмеження забезпечує узгодженість даних і запобігає несподіваним побічним ефектам. Дані беруться з бази даних, і будь-які зміни повинні проводитися явно і контрольованим чином. + + +`foreach` - Ітерація по всіх рядках +----------------------------------- -Приклади використання: +Найпростіший спосіб виконати запит і отримати рядки - це ітерація за допомогою циклу `foreach`. Він автоматично виконує SQL-запит. ```php -$table->having('COUNT(items) >', 100); // HAVING COUNT(`items`) > 100 +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key = первинний ключ, $book = ActiveRow + echo "$book->title ({$book->author->name})"; +} ``` -Фільтрація за іншим значенням таблиці .[#toc-joining-key] ---------------------------------------------------------- +get($key): ?ActiveRow .[method] +------------------------------- + +Виконує SQL-запит і повертає рядок за первинним ключем або `null`, якщо він не існує. + +```php +$book = $explorer->table('book')->get(123); // повертає ActiveRow з ідентифікатором 123 або null +if ($book) { + echo $book->title; +} +``` + -Досить часто потрібно відфільтрувати результати за будь-якою умовою, яка включає іншу таблицю бази даних. Для таких умов потрібні табличні з'єднання. Однак вам більше не потрібно їх писати. +fetch(): ?ActiveRow .[method] +----------------------------- -Припустимо, вам потрібно отримати всі книги, ім'я автора яких 'Jon'. Усе, що вам потрібно написати, це з'єднувальний ключ відношення та ім'я стовпця в об'єднаній таблиці. Ключ об'єднання береться зі стовпця, який посилається на таблицю, до якої ви хочете приєднатися. У нашому прикладі (див. схему db) це стовпчик `author_id`, і достатньо використовувати тільки його першу частину - `author` (суфікс `_id` можна опустити). `name` - це стовпець у таблиці `author`, який ми хочемо використовувати. Умова для перекладача книги (яка пов'язана з колонкою `translator_id`) може бути створена так само просто. +Повертає один рядок і переводить внутрішній покажчик на наступний. Якщо рядків більше немає, повертається `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); +} ``` -Логіка сполучних ключів визначається реалізацією [Conventions |api:Nette\Database\Conventions]. Ми рекомендуємо використовувати [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], який аналізує ваші зовнішні ключі та дозволяє легко працювати з цими відносинами. -Відносини між книгою та її автором - 1:N. Зворотні відносини також можливі. Ми називаємо це **зворотним з'єднанням**. Погляньте на інший приклад. Ми хочемо отримати всіх авторів, які написали понад 3 книги. Щоб зробити з'єднання зворотним, ми використовуємо `:` (двоеточие). Двоеточие означает, что объединенное отношение имеет значение hasMany (и это вполне логично, так как две точки больше, чем одна). К сожалению, класс Selection недостаточно умен, поэтому мы должны помочь с агрегацией и предоставить оператор `GROUP BY`, також умова має бути записана у вигляді оператора `HAVING`. +fetchPairs(): array .[method] +----------------------------- + +Повертає результати у вигляді асоціативного масиву. У першому аргументі вказується ім'я стовпця, яке буде використовуватися в якості ключа масиву, а в другому - ім'я стовпця, яке буде використовуватися в якості значення: ```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', ...] ``` -Ви, напевно, помітили, що вираз з'єднання відноситься до книги, але незрозуміло, чи об'єднуємо ми через `author_id` або `translator_id`. У наведеному вище прикладі Selection з'єднується через стовпчик `author_id`, тому що знайдено збіг з вихідною таблицею - таблицею `author`. Якби такого збігу не було, і було б більше можливостей, Nette викинув би [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Якщо вказано тільки ключовий стовпець, то значенням буде весь рядок, тобто об'єкт `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` -Щоб виконати об'єднання через колонку `translator_id`, надайте необов'язковий параметр у виразі об'єднання. +Якщо в якості ключа вказано `null`, то масив буде мати числовий індекс, починаючи з нуля: ```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', ...] +``` + +В якості параметра можна також передати зворотний виклик, який поверне або саме значення, або пару ключ-значення для кожного ряду. Якщо зворотний виклик повертає тільки значення, то ключем буде первинний ключ рядка: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'Перша книга (Ян Новак)', ...]. + +// Зворотний виклик також може повертати масив з парою "ключ і значення": +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['Перша книга' => 'Ян Новак', ...]. ``` -Давайте розглянемо складніший вираз приєднання. -Ми хочемо знайти всіх авторів, які написали щось про PHP. Усі книжки мають теги, тому ми повинні вибрати тих авторів, які написали будь-яку книжку з тегом PHP. +fetchAll(): array .[method] +--------------------------- + +Повертає всі рядки у вигляді асоціативного масиву об'єктів `ActiveRow`, де ключами є значення первинного ключа. ```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, ...), ...] ``` -Агреговані запити .[#toc-aggregate-queries] -------------------------------------------- +count(): int .[method] +---------------------- -| `$table->count('*')` | Отримуємо кількість рядків -| `$table->count("DISTINCT $column")` | Отримуємо кількість окремих значень -| `$table->min($column)` | Отримуємо мінімальне значення -| `$table->max($column)` | Отримуємо максимальне значення -| `$table->sum($column)` | Отримуємо суму всіх значень -| `$table->aggregation("GROUP_CONCAT($column)")` | Запускаємо будь-яку функцію агрегації +Метод `count()` без параметрів повертає кількість рядків в об'єкті `Selection`: -.[caution] -Метод `count()` без зазначення параметрів вибирає всі записи і повертає розмір масиву, що дуже неефективно. Наприклад, якщо вам потрібно підрахувати кількість рядків для пейджингу, завжди вказуйте перший аргумент. +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // альтернатива +``` +Примітка: `count()` з параметром виконує функцію агрегування COUNT в базі даних, як описано нижче. -Екранування та лапки .[#toc-escaping-quoting] -============================================= -Database Explorer розумний і позбудеться параметрів та ідентифікаторів лапок за вас. Проте необхідно дотримуватися таких основних правил: +ActiveRow::toArray(): array .[method] +------------------------------------- -- ключові слова, функції, процедури мають бути у верхньому регістрі -- стовпці й таблиці мають бути в нижньому регістрі -- передавайте змінні як параметри, не об'єднуйте їх +Перетворює об'єкт `ActiveRow` в асоціативний масив, ключами якого є імена стовпців, а значеннями - відповідні дані. ```php -->where('name like ?', 'John'); // НЕПРАВИЛЬНО! Генерує: `name` `like` ? -->where('name LIKE ?', 'John'); // ПРАВИЛЬНО +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray складатиметься з ['id' => 1, 'title' => '...', 'author_id' => ..., ...]. +``` + -->where('KEY = ?', $value); // НЕПРАВИЛЬНО! КЛЮЧ - це ключове слово -->where('key = ?', $value); // ПРАВИЛЬНО. Генерує: `key` = ? +Агрегація .[#toc-aggregation] +============================= -->where('name = ' . $name); // Неправильно! sql-ін'єкція! -->where('name = ?', $name); // ПРАВИЛЬНО +Клас `Selection` надає методи для зручного виконання функцій агрегування (COUNT, SUM, MIN, MAX, AVG і т. д.). -->select('DATE_FORMAT(created, "%d.%m.%Y")'); // НЕПРАВИЛЬНО! Передавайте змінні як параметри, не конкатеніруйте -->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // ПРАВИЛЬНО +.[language-php] +| `count($expr)` | Підраховує кількість рядків | +| `min($expr)` | Повертає мінімальне значення в стовпці | +| `max($expr)` | Повертає максимальне значення в стовпці | +| `sum($expr)` | Повертає суму значень в стовпці | +| `aggregation($function)` | Дозволяє використовувати будь-яку функцію агрегування, наприклад `AVG()` або `GROUP_CONCAT()` | + + +count(string $expr): int .[method] +---------------------------------- + +Виконує SQL-запит з функцією COUNT і повертає результат. Цей метод використовується для визначення кількості рядків, що відповідають певній умові: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Примітка: функція [count() |#count()] без параметра просто повертає кількість рядків в об'єкті `Selection`. + + +min(string $expr) and max(string $expr) .[method] +------------------------------------------------- + +Методи `min()` і `max()` повертають мінімальне і максимальне значення в зазначеному стовпці або виразі: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr): int .[method] +-------------------------------- + +Повертає суму значень в зазначеному стовпці або виразі: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); ``` -.[warning] -Неправильне використання може призвести до утворення дірок у безпеці +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- + +Дозволяє виконати будь-яку агрегатну функцію. -Отримання даних .[#toc-fetching-data] -===================================== +```php +// Обчислює середню ціну товарів в категорії +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); -| `foreach ($table as $id => $row)` | Ітерація по всіх рядках результату -| `$row = $table->get($id)` | Отримуємо один рядок з ідентифікатором $id з таблиці -| `$row = $table->fetch()` | Отримуємо наступний рядок із результату -| `$array = $table->fetchPairs($key, $value)` | Вибірка всіх значень у вигляді асоціативного масиву -| `$array = $table->fetchPairs($value)` | Вибірка всіх рядків у вигляді асоціативного масиву -| `count($table)` | Отримуємо кількість рядків у результуючому наборі +// Об'єднує теги товарів в один рядок +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Якщо нам потрібно агрегувати результати, які самі є результатом агрегування і групування (наприклад, `SUM(value)` над згрупованими рядками), то в якості другого аргументу ми вказуємо функцію агрегування, яка буде застосовуватися до цих проміжних результатів: + +```php +// Розраховує загальну ціну товарів на складі для кожної категорії, потім підсумовує ці ціни +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +У цьому прикладі ми спочатку обчислюємо загальну ціну товарів в кожній категорії (`SUM(price * stock) AS category_total`) і групуємо результати за `category_id`. Потім ми використовуємо `aggregation('SUM(category_total)', 'SUM')` для підсумовування цих проміжних підсумків. Другий аргумент `'SUM'` задає функцію агрегування, яку потрібно застосувати до проміжних результатів. Вставка, оновлення та видалення .[#toc-insert-update-delete] ============================================================ -Метод `insert()` приймає масив об'єктів Traversable (наприклад, [ArrayHash |utils:arrays#ArrayHash], який повертає [forms |forms:]): +Nette Database Explorer спрощує вставку, оновлення та видалення даних. Всі перераховані методи викидають повідомлення `Nette\Database\DriverException` у разі помилки. + + +Selection::insert(iterable $data): static .[method] +--------------------------------------------------- + +Вставляє нові записи в таблицю. + +**Вставка одного запису:**. + +Новий запис передається у вигляді асоціативного масиву або ітерабельного об'єкта (наприклад, `ArrayHash`, що використовується в [формах |forms:]), де ключі відповідають іменам стовпців в таблиці. + +Якщо таблиця має визначений первинний ключ, метод повертає об'єкт `ActiveRow`, який перезавантажується з бази даних, щоб відобразити будь-які зміни, зроблені на рівні бази даних (наприклад, тригери, значення стовпців за замовчуванням або обчислення з автоінкрементами). Це забезпечує узгодженість даних, і об'єкт завжди містить поточні дані бази даних. Якщо первинний ключ не визначено явно, метод повертає вхідні дані у вигляді масиву. ```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 - це екземпляр ActiveRow, що містить повні дані вставленого ряду, +// включаючи автоматично згенерований ідентифікатор і будь-які зміни, зроблені тригерами +echo $row->id; // Виводить ідентифікатор нового вставленого користувача +echo $row->created_at; // Виводить час створення, якщо він встановлений тригером ``` -Якщо для таблиці визначено первинний ключ, повертається об'єкт ActiveRow, що містить вставлений рядок. +**Вставка декількох записів одночасно:**. -Вставлення кількох значень: +Метод `insert()` дозволяє вставити кілька записів за допомогою одного SQL-запиту. У цьому випадку він повертає кількість вставлених рядків. ```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 буде дорівнювати 2 +``` + +В якості параметра можна також передати об'єкт `Selection` з вибіркою даних. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); ``` -Як параметри можна передавати файли або об'єкти DateTime: +**Вставка спеціальних значень:** + +Значення можуть включати файли, об'єкти `DateTime` або літерали SQL: ```php $explorer->table('users')->insert([ - 'name' => $name, - 'created' => new DateTime, // или $explorer::literal('NOW()') - 'avatar' => fopen('image.gif', 'r'), // вставляет файл + 'name' => 'John', + 'created_at' => new DateTime, // перетворення в формат бази даних + 'avatar' => fopen('image.jpg', 'rb'), // вставляє вміст бінарного файлу + 'uuid' => $explorer::literal('UUID()'), // викликає функцію UUID() ]); ``` -Оновлення (повертає кількість порушених рядків): + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Оновлює рядки в таблиці на основі заданого фільтра. Повертає кількість фактично змінених рядків. + +Оновлювані стовпці передаються у вигляді асоціативного масиву або ітерабельного об'єкта (наприклад, `ArrayHash`, що використовується в [формах |forms:]), де ключі відповідають іменам стовпців в таблиці: ```php -$count = $explorer->table('users') - ->where('id', 10) // должен вызываться до 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 ``` -Для оновлення ми можемо використовувати оператори `+=` і `-=`: +Для зміни числових значень можна використовувати оператори `+=` і `-=`: ```php $explorer->table('users') + ->where('id', 10) ->update([ - 'age+=' => 1, // see += + 'points+=' => 1, // збільшує значення стовпця "points" на 1 + 'coins-=' => 1, // зменшує значення стовпця 'coins' на 1 ]); -// UPDATE users SET `age` = `age` + 1 +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` -Видалення (повертає кількість видалених рядків): + +Selection::delete(): int .[method] +---------------------------------- + +Видаляє рядки з таблиці на основі заданого фільтра. Повертає кількість видалених рядків. ```php $count = $explorer->table('users') ->where('id', 10) ->delete(); -// DELETE FROM `users` WHERE (`id` = 10) +// DELETE FROM `users` WHERE `id` = 10 ``` +.[caution] +При виклику `update()` або `delete()` обов'язково використовуйте `where()` для вказівки рядків, що оновлюються або видаляються. Якщо `where()` не використовується, операція буде виконана над усією таблицею! -Робота з відносинами .[#toc-working-with-relationships] -======================================================= +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- -Один до одного ("has one") .[#toc-has-one-relation] ---------------------------------------------------- -Відношення "Один до одного" - поширений випадок використання. Книга *має одного* автора. Книга *має одного* перекладача. Отримання зв'язаного рядка в основному здійснюється методом `ref()`. Він приймає два аргументи: ім'я цільової таблиці та стовпець вихідного з'єднання. Див. приклад: +Оновлює дані в рядку бази даних, представленому об'єктом `ActiveRow`. В якості параметра він приймає ітерабельні дані, де ключами є імена стовпців. Для зміни числових значень можна використовувати оператори `+=` і `-=`: + +Після виконання оновлення `ActiveRow` автоматично перезавантажується з бази даних, щоб відобразити всі зміни, зроблені на рівні бази даних (наприклад, тригерами). Метод повертає `true` тільки в тому випадку, якщо відбулася реальна зміна даних. ```php -$book = $explorer->table('book')->get(1); -$book->ref('author', 'author_id'); +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // збільшує кількість переглядів +]); +echo $article->views; // Виводить поточну кількість переглядів ``` -У наведеному вище прикладі ми витягуємо пов'язаний запис про автора з таблиці `author`, пошук первинного ключа автора здійснюється за стовпцем `book.author_id`. Метод Ref() повертає екземпляр ActiveRow або null, якщо немає відповідного запису. Повернутий рядок є екземпляром ActiveRow, тому ми можемо працювати з ним так само, як і з записом книги. +Цей метод оновлює тільки один конкретний рядок в базі даних. Для масового оновлення декількох рядків використовуйте метод [Selection::update() |#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Видаляє з бази даних рядок, представлений об'єктом `ActiveRow`. ```php -$author = $book->ref('author', 'author_id'); -$author->name; -$author->born; +$book = $explorer->table('book')->get(1); +$book->delete(); // Видалення книги з ідентифікатором 1 +``` + +Цей метод видаляє тільки один конкретний ряд в базі даних. Для масового видалення декількох рядків використовуйте метод [Selection::delete() |#Selection::delete()]. -// або напряму -$book->ref('author', 'author_id')->name; -$book->ref('author', 'author_id')->born; + +Відносини між таблицями .[#toc-relationships-between-tables] +============================================================ + +У реляційних базах даних дані розділені на кілька таблиць і пов'язані між собою за допомогою зовнішніх ключів. Nette Database Explorer пропонує революційний спосіб роботи з цими відносинами - без написання запитів JOIN і без необхідності конфігурування або генерації сутностей. + +Для демонстрації ми скористаємося базою даних **example**[(доступна на GitHub |https://github.com/nette-examples/books]). База даних включає в себе наступні таблиці: + +- `author` - автори і перекладачі (стовпці `id`, `name`, `web`, `born`) +- `book` - книги (стовпці `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - теги (колонки `id`, `name`) +- `book_tag` - таблиця зв'язків між книгами і тегами (колонки `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Структура бази даних .<> + +У цьому прикладі бази даних книг ми бачимо кілька типів зв'язків (спрощених в порівнянні з реальністю): + +- **Один-до-багатьох (1:N)** - У кожної книги **є один** автор; автор може написати **безліч** книг. +- **Нуль-до-багатьох (0:N)** - У книги **може бути** перекладач; перекладач може перекласти **безліч** книг. +- **Нуль-до-одного (0:1)** - Книга **може мати** продовження. +- **Багато-до-багатьох (M:N)** - Книга **може мати кілька** тегів, і один тег може бути присвоєний **декільком** книгам. + +У цих відносинах завжди є **батьківська таблиця** і **дочірня таблиця**. Наприклад, у відносинах між авторами і книгами таблиця `author` є батьківською, а таблиця `book` - дочірньою - можна вважати, що книга завжди "належить" одному автору. Це також відображено в структурі бази даних: дочірня таблиця `book` містить зовнішній ключ `author_id`, який посилається на батьківську таблицю `author`. + +Якщо ми хочемо відобразити книги разом з іменами їх авторів, у нас є два варіанти. Або ми отримуємо дані за допомогою одного SQL-запиту з JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; +``` + +Або ми отримуємо дані в два етапи - спочатку книги, потім їх авторів - і збираємо їх в PHP: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books ``` -Книга також має одного перекладача, тому дізнатися ім'я перекладача досить просто. +Другий підхід, як не дивно, **більш ефективний**. Дані витягуються тільки один раз і можуть бути краще використані в кеші. Саме так працює Nette Database Explorer - він обробляє все під капотом і надає вам чистий 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 - це запис з таблиці 'author'. + echo 'translated by: ' . $book->translator?->name; +} ``` -Усе це добре, але дещо громіздко, чи не так? Database Explorer уже містить визначення зовнішніх ключів, то чому б не використовувати їх автоматично? Давайте зробимо це! -Якщо ми викликаємо властивість, якої не існує, ActiveRow намагається дозволити ім'я властивості, що викликає, як відношення 'has one'. Отримання цієї властивості аналогічно виклику методу ref() тільки з одним аргументом. Ми будемо називати єдиний аргумент **key**. Ключ буде дозволено в конкретне відношення зовнішнього ключа. Переданий ключ зіставляється зі стовпчиками рядка, і якщо він збігається, то зовнішній ключ, визначений у зіставленому стовпчику, використовується для отримання даних із пов'язаної цільової таблиці. Див. приклад: +Доступ до батьківської таблиці .[#toc-accessing-the-parent-table] +----------------------------------------------------------------- + +Доступ до батьківської таблиці дуже простий. Це такі відносини, як *у книги є автор* або *у книги може бути перекладач*. Доступ до пов'язаного запису можна отримати через властивість об'єкта `ActiveRow` - ім'я властивості збігається з ім'ям стовпця зовнішнього ключа без суфікса `id`: ```php -$book->author->name; -// те ж саме -$book->ref('author')->name; +$book = $explorer->table('book')->get(1); +echo $book->author->name; // знаходить автора за стовпцем 'author_id' +echo $book->translator?->name; // знаходить перекладача за стовпцем 'translator_id' ``` -Примірник ActiveRow не має колонки автора. Усі стовпці книги шукаються на предмет збігу з *key*. Збіг у цьому випадку означає, що ім'я стовпця має містити ключ. Так, у наведеному прикладі стовпець `author_id` містить рядок 'author' і тому зіставляється з ключем 'author'. Якщо ви хочете отримати перекладача книги, то як ключ можна використовувати, наприклад, 'translator', оскільки ключ 'translator' відповідатиме стовпчику `translator_id`. Докладніше про логіку підбору ключів ви можете прочитати в розділі [Joining expressions |#joining-key]. +При зверненні до властивості `$book->author` Explorer шукає в таблиці `book` стовпець, що містить рядок `author` (наприклад, `author_id`). На підставі значення в цьому стовпці він витягує відповідний запис з таблиці `author` і повертає його у вигляді об'єкта `ActiveRow`. Аналогічно, `$book->translator` використовує стовпець `translator_id`. Оскільки стовпець `translator_id` може містити `null`, використовується оператор `?->`. + +Альтернативний підхід забезпечується методом `ref()`, який приймає два аргументи - ім'я цільової таблиці і зв'язуючий стовпець - і повертає екземпляр `ActiveRow` або `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; // посилання на автора +echo $book->ref('author', 'translator_id')->name; // посилання на перекладача ``` -Якщо ви хочете отримати кілька книг, використовуйте той самий підхід. Nette Database Explorer знайде авторів і перекладачів одразу для всіх знайдених книг. +Метод `ref()` корисний, якщо доступ на основі властивостей не може бути використаний, наприклад, коли таблиця містить стовпець з тим же ім'ям, що і властивість (`author`). В інших випадках рекомендується використовувати доступ на основі властивостей для кращої читабельності. + +Explorer автоматично оптимізує запити до бази даних. При ітерації книг і доступі до пов'язаних з ними записів (автори, перекладачі) Explorer не генерує запит для кожної книги окремо. Замість цього він виконує тільки **один запит SELECT для кожного типу відносин**, що значно знижує навантаження на базу даних. Наприклад: ```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; } ``` -Код виконуватиме лише ці 3 запити: +Цей код виконає тільки три оптимізованих запити до бази даних: + ```sql SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ids of fetched books from author_id column -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ids of fetched books from translator_id column +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from 'translator_id' column in selected books ``` +.[note] +Логіка визначення зв'язуючого стовпця визначається реалізацією [Conventions |api:Nette\Database\Conventions]. Ми рекомендуємо використовувати [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], яка аналізує зовнішні ключі і дозволяє безперешкодно працювати з існуючими зв'язками таблиць. + -Один до багатьох ("has many") .[#toc-has-many-relation] -------------------------------------------------------- +Доступ до дочірньої таблиці .[#toc-accessing-the-child-table] +------------------------------------------------------------- -Відношення "один до багатьох" - це просто зворотне відношення "один до одного". Автор *написав* *багато* книг. Автор *переклав* *багато* книг. Як бачите, цей тип відношення трохи складніший, тому що відношення є "іменованим" ("написав", "переклав"). У екземпляра ActiveRow є метод `related()`, який повертає масив пов'язаних записів. Записи також є екземплярами ActiveRow. Див. приклад нижче: +Доступ до дочірньої таблиці працює в зворотному напрямку. Тепер ми запитуємо *які книги написав цей автор* або *які книги переклав цей перекладач*. Для цього типу запиту ми використовуємо метод `related()`, який повертає об'єкт `Selection` з відповідними записами. Ось приклад: ```php -$author = $explorer->table('author')->get(11); -echo $author->name . ' написал:'; +$author = $explorer->table('author')->get(1); +// Виводить всі книги, написані автором foreach ($author->related('book.author_id') as $book) { - echo $book->title; + echo "Wrote: $book->title"; } -echo 'и перевёл:'; +// Виводить всі книги, перекладені автором foreach ($author->related('book.translator_id') as $book) { - echo $book->title; + echo "Translated: $book->title"; } ``` -Метод `related()` приймає повний опис з'єднання, що передається як два аргументи або як один аргумент, з'єднаний крапкою. Перший аргумент - цільова таблиця, другий - цільовий стовпець. +Метод `related()` приймає опис відносини як один аргумент з використанням точкової нотації або як два окремих аргументи: + +```php +$author->related('book.translator_id'); // один аргумент +$author->related('book', 'translator_id'); // два аргументи +``` + +Explorer може автоматично визначити правильний стовпець зв'язку на основі імені батьківської таблиці. В даному випадку зв'язок встановлюється через стовпець `book.author_id`, оскільки ім'я вихідної таблиці - `author`: ```php -$author->related('book.translator_id'); -// те саме -$author->related('book', 'translator_id'); +$author->related('book'); // використовує book.author_id ``` -Ви можете використовувати евристику Nette Database Explorer, засновану на зовнішніх ключах, і вказати тільки аргумент **key**. Ключ буде зіставлено з усіма зовнішніми ключами, що вказують на поточну таблицю (таблиця `author`). Якщо є збіг, Nette Database Explorer буде використовувати цей зовнішній ключ, в іншому випадку він викине [Nette\InvalidArgumentException |api:Nette\InvalidArgumentException] або [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Детальніше про логіку підбору ключів ви можете прочитати в розділі [Joining expressions |#joining-key]. +Якщо існує кілька можливих зв'язків, Explorer викине виняток [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Звичайно, ви можете викликати пов'язані методи для всіх знайдених авторів, і Nette Database Explorer знову отримає відповідні книги одразу. +Звичайно, ми також можемо використовувати метод `related()` при циклічному переборі декількох записів, і Explorer автоматично оптимізує запити і в цьому випадку: ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' написал:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { - $book->title; + echo $book->title; } } ``` -У наведеному вище прикладі буде виконано лише два запити: +Цей код генерує тільки два ефективних SQL-запити: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- идентификаторы найденных авторов +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Створення Explorer вручну .[#toc-creating-explorer-manually] -============================================================ +Відносини "багато-до-багатьох" .[#toc-many-to-many-relationship] +---------------------------------------------------------------- + +Для відносин "багато-до-багатьох" (M:N) потрібна **таблиця-перехрестя** (в нашому випадку `book_tag`). Ця таблиця містить два стовпці із зовнішніми ключами (`book_id`, `tag_id`). Кожен стовпець посилається на первинний ключ однієї з пов'язаних таблиць. Щоб отримати пов'язані дані, ми спочатку витягуємо записи з таблиці зв'язків за допомогою `related('book_tag')`, а потім переходимо до цільових даних: + +```php +$book = $explorer->table('book')->get(1); +// Виводить імена тегів, присвоєних книзі +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // отримує назву тега через таблицю посилань +} -З'єднання з базою даних може бути створено за допомогою конфігурації програми. У таких випадках створюється служба `Nette\Database\Explorer`, яка може бути передана як залежність за допомогою DI-контейнера. +$tag = $explorer->table('tag')->get(1); +// Протилежний напрямок: виводить назви книг з даним тегом +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // отримує назву книги +} +``` -Однак, якщо Nette Database Explorer використовується як самостійний інструмент, екземпляр об'єкта `Nette\Database\Explorer` повинен бути створений вручну. +Explorer знову оптимізує SQL-запити в ефективну форму: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag +``` + + +Запит через пов'язані таблиці .[#toc-querying-through-related-tables] +--------------------------------------------------------------------- + +У методах `where()`, `select()`, `order()` і `group()` можна використовувати спеціальні позначення для доступу до стовпців з інших таблиць. Explorer автоматично створює необхідні JOIN. + +**Точкова нотація** (`parent_table.column`) використовується для відносин 1:N з точки зору батьківської таблиці: ```php -// $storage реалізує Nette\Caching\Storage: -$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); +$books = $explorer->table('book'); + +// Знаходить книги, імена авторів яких починаються з "Jon". +$books->where('author.name LIKE ?', 'Jon%'); + +// Сортує книги за ім'ям автора за спаданням +$books->order('author.name DESC'); + +// Виводить назву книги та ім'я автора +$books->select('book.title, author.name'); +``` + +**Нотація з двокрапкою** (`:child_table.column`) використовується для відносин 1:N з точки зору дочірньої таблиці: + +```php +$authors = $explorer->table('author'); + +// Знаходить авторів, які написали книги з 'PHP' в назві +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Підраховує кількість книг для кожного автора +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +У наведеному вище прикладі з позначенням двокрапки (`:book.title`) стовпець зовнішнього ключа явно не вказано. Explorer автоматично визначає потрібний стовпець на основі імені батьківської таблиці. В даному випадку з'єднання виконується через стовпець `book.author_id`, оскільки ім'я вихідної таблиці - `author`. Якщо існує кілька можливих з'єднань, Explorer викидає виняток [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Зв'язуючий стовпець можна явно вказати в круглих дужках: + +```php +// Знаходить авторів, які переклали книгу з 'PHP' в назві +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Нотації можна об'єднувати в ланцюжки для доступу до даних в декількох таблицях: + +```php +// Пошук авторів книг, позначених тегом 'PHP'. +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Розширення умов для JOIN .[#toc-extending-conditions-for-join] +-------------------------------------------------------------- + +Метод `joinWhere()` додає додаткові умови до об'єднання таблиць в SQL після ключового слова `ON`. + +Наприклад, ми хочемо знайти книги, перекладені певним перекладачем: + +```php +// Знаходить книги, перекладені перекладачем на ім'я 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +В умові `joinWhere()` можна використовувати ті ж конструкції, що і в методі `where()`, - оператори, заповнювачі, масиви значень або вирази SQL. + +Для більш складних запитів з кількома JOIN можна визначити псевдоніми таблиць: + +```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) +``` + +Зверніть увагу, що якщо метод `where()` додає умови в речення `WHERE`, то метод `joinWhere()` розширює умови в реченні `ON` при об'єднанні таблиць. + + +Створення провідника вручну .[#toc-manually-creating-explorer] +============================================================== + +Якщо ви не використовуєте контейнер Nette DI, ви можете створити екземпляр `Nette\Database\Explorer` вручну: + +```php +use Nette\Database; + +// $storage реалізує Nette\Caching\Storage, наприклад: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// підключення до бази даних +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// управляє відображенням структури бази даних +$structure = new Database\Structure($connection, $storage); +// визначає правила зіставлення імен таблиць, стовпців і зовнішніх ключів +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); ``` diff --git a/database/uk/security.texy b/database/uk/security.texy new file mode 100644 index 0000000000..76d85bf7f3 --- /dev/null +++ b/database/uk/security.texy @@ -0,0 +1,145 @@ +Ризики безпеки +************** + +.[perex] +Бази даних часто містять конфіденційні дані і дозволяють виконувати небезпечні операції. Nette Database надає ряд функцій безпеки. Однак дуже важливо розуміти різницю між безпечними та небезпечними API. + + +Ін'єкція SQL .[#toc-sql-injection] +================================== + +SQL-ін'єкція є найсерйознішим ризиком безпеки при роботі з базами даних. Вона виникає, коли неконтрольоване введення користувача стає частиною SQL-запиту. Зловмисник може вставити свої власні SQL-команди, отримуючи або змінюючи дані в базі даних. + +```php +// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL ін'єкцій +$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); + +// Зловмисник може ввести щось на кшталт ' OR '1'='1 +// Результуючий запит буде виглядати наступним чином: +// SELECT * FROM users WHERE name = '' OR '1'='1' +// Це поверне всіх користувачів! +``` + +Те ж саме стосується і Провідника баз даних: + +```php +// ❌ КОД НЕБЕЗПЕКИ +$table->where('name = ' . $_GET['name']); +$table->where("name = '$_GET[name]'"); +``` + + +Безпечні параметризовані запити .[#toc-safe-parameterized-queries] +================================================================== + +Безпечний спосіб вставляти значення в SQL-запити - це параметризовані запити. Nette Database надає кілька способів їх використання. + + +Знаки питання-заповнювачі .[#toc-placeholder-question-marks] +------------------------------------------------------------ + +Найпростіший метод - використання знаків питання-заповнювачів: + +```php +// ✅ Безпечні параметризовані запити +$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); + +// ✅ Безпечний стан в Провіднику +$table->where('name = ?', $_GET['name']); +``` + +Те саме стосується всіх інших методів у Провіднику баз даних, які дозволяють вставляти вирази зі знаками питання-заповнювачами та параметрами. + +.[warning] +Значення повинні бути скалярного типу (`string`, `int`, `float`, `bool`) або `null`. Якщо, наприклад, `$_GET['name']` є масивом, Nette Database включить всі його елементи в SQL-запит, що може бути небажаним. + + +Масиви значень .[#toc-value-arrays] +----------------------------------- + +Для речень `INSERT`, `UPDATE` або `WHERE` ми можемо використовувати масиви значень: + +```php +// ✅ Безпечна ВСТАВКА +$database->query('INSERT INTO users', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +]); + +// Безпечне Оновлення +$database->query('UPDATE users SET', [ + 'name' => $_GET['name'], + 'email' => $_GET['email'], +], 'WHERE id = ?', $_GET['id']); +``` + +База даних Nette автоматично екранує всі значення, передані через параметризовані запити. Однак, ми повинні переконатися, що параметри мають правильний тип даних. + + +Масивні ключі не є безпечним API .[#toc-array-keys-are-not-a-safe-api] +====================================================================== + +Хоча значення в масивах є безпечними, цього не можна сказати про ключі: + +```php +// ❌ НЕБЕЗПЕЧНИЙ КОД - ключі можуть містити SQL-ін'єкції +$database->query('INSERT INTO users', $_GET); +$database->query('SELECT * FROM users WHERE', $_GET); +$table->where($_GET); +``` + +Для команд `INSERT` і `UPDATE` це критичний недолік безпеки - зловмисник може вставити або змінити будь-який стовпець в базі даних. Наприклад, він може встановити `is_admin = 1` або вставити довільні дані в конфіденційні стовпці. + +В умовах `WHERE` це ще більш небезпечно, оскільки дозволяє **SQL-перебір** - техніку поступового отримання інформації про базу даних. Зловмисник може спробувати дослідити зарплати співробітників, вставивши дані в `$_GET` таким чином: + +```php +$_GET = ['salary >', 100000]; // починає визначати діапазони заробітних плат +``` + +Основна проблема, однак, полягає в тому, що умови `WHERE` підтримують SQL-вирази в ключах: + +```php +// Легальне використання операторів у ключах +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], +]); + +// НЕБЕЗПЕЧНО: зловмисник може вставити свій власний SQL +$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; +$table->where($_GET); // дозволяє зловмиснику отримувати зарплату адміністратора +``` + +Це знову ж таки **SQL ін'єкція**. + + +Стовпці білого списку .[#toc-whitelisting-columns] +-------------------------------------------------- + +Якщо ви хочете дозволити користувачам вибирати колонки, завжди використовуйте білий список: + +```php +// ✅ Безпечна обробка - тільки дозволені стовпці +$allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_GET, array_flip($allowedColumns)); + +$database->query('INSERT INTO users', $values); +``` + + +Динамічні ідентифікатори .[#toc-dynamic-identifiers] +==================================================== + +Для динамічних назв таблиць і стовпців використовуйте заповнювач `?name`: + +```php +// ✅ Безпечне використання довірених ідентифікаторів +$table = 'users'; +$column = 'name'; +$database->query('SELECT ?name FROM ?name', $column, $table); + +// НЕБЕЗПЕЧНО - ніколи не використовуйте введення користувача +$database->query('SELECT ?name FROM users', $_GET['column']); +``` + +Символ `?name` слід використовувати лише для довірених значень, визначених у коді програми. Для значень, наданих користувачем, знову використовуйте білий список.