Изследовател на бази данни

С Nette Database можете да работите по два начина: да оставите SQL заявките да се генерират автоматично (подход на изследователя) или да ги напишете сами(директен достъп). Изследователят значително опростява достъпа до данните. Той обработва връзките между таблиците, така че можете да се съсредоточите върху логиката на вашето приложение.

  • Работата с данни е естествена и лесна за разбиране
  • Генерира оптимизирани SQL заявки, които извличат само необходимите данни
  • Осигурява лесен достъп до свързани данни, без да е необходимо да се пишат JOIN заявки
  • Работи незабавно без конфигуриране или генериране на ентитети

Работата с Изследователя започва с извикване на метода table() на обекта Nette\Database\Explorer (за информация относно създаването на връзки и конфигурацията вижте специалната страница):

$books = $explorer->table('book'); // 'book' е името на таблицата

Методът връща обект Selection, който представлява SQL заявка. Към този обект могат да бъдат верижно свързани допълнителни методи за филтриране и сортиране на резултатите. Запитването се събира и изпълнява само когато се изискват данни, например чрез итерация с foreach. Всеки ред се представя от обект ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // извежда колона 'title'
	echo $book->author_id;    // извежда колона 'author_id'
}

Explorer значително опростява работата с връзките между таблиците. Следващият пример показва колко лесно можем да изведем данни от свързани таблици (книги и техните автори). Обърнете внимание, че не е необходимо да се пишат заявки JOIN; Nette ги генерира вместо нас:

$books = $explorer->table('book');

foreach ($books as $book) {
	echo 'Book: ' . $book->title;
	echo 'Author: ' . $book->author->name; // създава JOIN към таблицата "author".
}

Nette Database Explorer оптимизира заявките за максимална ефективност. В горния пример се изпълняват само две SELECT заявки, независимо дали обработваме 10 или 10 000 книги.

Освен това Explorer проследява кои колони се използват в кода и извлича само тях от базата данни, като по този начин спестява допълнително производителност. Това поведение е напълно автоматично и адаптивно. Ако по-късно промените кода, за да използвате допълнителни колони, Explorer автоматично коригира заявките. Не е необходимо да конфигурирате каквото и да било или да мислите кои колони ще бъдат необходими – оставете това на Nette.

Филтриране и сортиране

Класът Selection предоставя методи за филтриране и сортиране на данни.

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 за филтриране на групираните редове

Методите могат да бъдат верижно свързани (т.нар. плавен интерфейс): $table->where(...)->order(...)->limit(...).

Тези методи също така позволяват използването на специални обозначения за достъп до данни от свързани таблици.

Избягване и идентификатори

Методите автоматично есквапират параметрите и цитират идентификаторите (имена на таблици и колони), като предотвратяват SQL инжекции. За да се гарантира правилното функциониране, трябва да се спазват няколко правила:

  • Записвайте ключови думи, имена на функции, процедури и т.н. с uppercase.
  • Записвайте имената на колони и таблици с малки букви.
  • Винаги предавайте низове с помощта на параметри.
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(`име`) = ?

where (string|array $condition, …$parameters)static

Филтриране на резултатите чрез условията WHERE. Силата му се състои в интелигентната обработка на различни типове стойности и автоматичния избор на SQL оператори.

Основна употреба:

$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 се справя с тях вместо вас:

$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

Методът се справя правилно и с отрицателни условия и празни масиви:

$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); // ПРЕДУПРЕЖДЕНИЕ: Този синтаксис не се поддържа

Можете също така да подадете резултата от друга заявка за таблица като параметър, създавайки подзаявка:

// 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:

// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`)
$table->where([
	'price_final &lt; price_original',
	'stock_count > min_stock',
]);

В масива могат да се използват двойки ключ-стойност, като Nette отново ще избере автоматично правилните оператори:

// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3))
$table->where([
	'status' => 'active',
	'id' => [1, 2, 3],
]);

Можем също така да смесваме SQL изрази със заместители и множество параметри. Това е полезно за сложни условия с точно определени оператори:

// WHERE (`възраст` > 18) AND (ROUND(`score`, 2) > 75.5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // два параметъра се предават като масив
]);

Многократните извиквания на where() автоматично комбинират условията с помощта на AND.

whereOr (array $parameters)static

Подобно на where(), но комбинира условия с помощта на OR:

// WHERE (`status` = 'active') OR (`deleted` = 1)
$table->whereOr([
	'status' => 'active',
	'deleted' => true,
]);

Могат да се използват и по-сложни изрази:

// WHERE (`price` > 1000) OR (`price_with_tax` > 1500)
$table->whereOr([
	'price > ?' => 1000,
	'price_with_tax > ?' => 1500,
]);

wherePrimary (mixed $key)static

Добавя условие за първичния ключ на таблицата:

// WHERE `id` = 123
$table->wherePrimary(123);

// WHERE `id` IN (1, 2, 3)
$table->wherePrimary([1, 2, 3]);

Ако таблицата има съставен първичен ключ (напр. foo_id, bar_id), го предаваме като масив:

// 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 (string $columns, …$parameters)static

Посочва реда, в който се връщат редовете. Можете да сортирате по една или повече колони, във възходящ или низходящ ред, или по потребителски израз:

$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 (string $columns, …$parameters)static

Посочва колоните, които да бъдат върнати от базата данни. По подразбиране Nette Database Explorer връща само колоните, които действително се използват в кода. Използвайте метода select(), когато трябва да извлечете конкретни изрази:

// 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:

foreach ($table as $row) {
	echo $row->formatted_date;   // достъп до псевдонима
}

limit (?int $limit, ?int $offset = null)static

Ограничава броя на върнатите редове (LIMIT) и по желание задава отместване:

$table->limit(10);        // LIMIT 10 (връща първите 10 реда)
$table->limit(10, 20);    // LIMIT 10 OFFSET 20

За страниране е по-подходящо да се използва методът page().

page (int $page, int $itemsPerPage, &$numOfPages = null)static

Опростява странирането на резултатите. Приема номера на страницата (започвайки от 1) и броя на елементите на страница. По желание можете да подадете препратка към променлива, в която ще се съхранява общият брой страници:

$numOfPages = null;
$table->page(page: 3, itemsPerPage: 10, $numOfPages);
echo "Total pages: $numOfPages";

group (string $columns, …$parameters)static

Групира редове по зададените колони (GROUP BY). Обикновено се използва в комбинация с функции за агрегиране:

// Отчита броя на продуктите във всяка категория
$table->select('category_id, COUNT(*) AS count')
	->group('category_id');

having (string $having, …$parameters)static

Задава условие за филтриране на групирани редове (HAVING). Може да се използва в комбинация с метода group() и функциите за агрегиране:

// Намира категории с повече от 100 продукта
$table->select('category_id, COUNT(*) AS count')
	->group('category_id')
	->having('count > ?', 100);

Четене на данни

За четене на данни от базата данни са налични няколко полезни метода:

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 е само за четене. Това означава, че не можете да променяте стойностите на неговите свойства. Това ограничение осигурява последователност на данните и предотвратява неочаквани странични ефекти. Данните се извличат от базата данни и всякакви промени трябва да се правят изрично и по контролиран начин.

foreach – Итериране през всички редове

Най-лесният начин за изпълнение на заявка и извличане на редове е чрез итерация с цикъла foreach. Той автоматично изпълнява SQL заявката.

$books = $explorer->table('book');
foreach ($books as $key => $book) {
	// $key = първичен ключ, $book = ActiveRow
	echo "$book->title ({$book->author->name})";
}

get ($key): ?ActiveRow

Изпълнява SQL заявка и връща ред по първичния му ключ или null, ако той не съществува.

$book = $explorer->table('book')->get(123); // връща ActiveRow с ID 123 или null
if ($book) {
	echo $book->title;
}

fetch(): ?ActiveRow

Връща един ред и придвижва вътрешния указател към следващия. Ако няма повече редове, се връща null.

$books = $explorer->table('book');
while ($book = $books->fetch()) {
	$this->processBook($book);
}

fetchPairs (string|int|null $key = null, string|int|null $value = null)array

Връща резултатите като асоциативен масив. Първият аргумент указва името на колоната, която ще се използва като ключ на масива, а вторият аргумент указва името на колоната, която ще се използва като стойност:

$authors = $explorer->table('author')->fetchPairs('id', 'name');
// [1 => "John Doe", 2 => "Jane Doe", ...]

Ако е предоставен само първият параметър, като стойност ще се използва целият ред, представен като обект ActiveRow:

$authors = $explorer->table('author')->fetchPairs('id');
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]

Ако като ключ е посочен null, масивът ще бъде индексиран цифрово, като се започне от нула:

$authors = $explorer->table('author')->fetchPairs(null, 'name');
// [0 => "John Doe", 1 => "Jane Doe", ...]

fetchPairs (Closure $callback)array

Алтернативно можете да подадете обратна връзка като параметър. Обратното извикване ще бъде приложено към всеки ред, за да върне или единична стойност, или двойка ключ-стойност.

$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]);
// ["Първа книга" => "Ян Новак", ...]

fetchAll(): array

Връща всички редове като асоциативен масив от обекти ActiveRow, където ключовете са стойностите на първичния ключ.

$allBooks = $explorer->table('book')->fetchAll();
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]

count(): int

Методът count() без параметри връща броя на редовете в обекта Selection:

$table->where('category', 1);
$count = $table->count();
$count = count($table); // алтернатива

Забележка: Методът count() с параметър изпълнява функцията за агрегиране COUNT в базата данни, както е описано по-долу.

ActiveRow::toArray(): array

Преобразува обекта ActiveRow в асоциативен масив, в който ключовете са имена на колони, а стойностите – съответните данни.

$book = $explorer->table('book')->get(1);
$bookArray = $book->toArray();
// $bookArray ще бъде ['id' => 1, 'title' => '...', 'author_id' => ..., ...]

Агрегиране

Класът Selection предоставя методи за лесно изпълнение на функции за агрегиране (COUNT, SUM, MIN, MAX, AVG и др.).

count($expr) Преброява броя на редовете
min($expr) Връща минималната стойност в дадена колона
max($expr) Връща максималната стойност в дадена колона
sum($expr) Връща сумата от стойностите в дадена колона
aggregation($function) Позволява всякаква функция за агрегиране, като например AVG() или GROUP_CONCAT()

count (string $expr): int

Изпълнява SQL заявка с функцията COUNT и връща резултата. Този метод се използва, за да се определи колко реда отговарят на определено условие:

$count = $table->count('*');                 // SELECT COUNT(*) FROM `table`
$count = $table->count('DISTINCT column');   // SELECT COUNT(DISTINCT `column`) FROM `table`

Забележка: count() без параметър просто връща броя на редовете в обекта Selection.

min (string $expr) and max(string $expr)

Методите min() и max() връщат минималните и максималните стойности в посочената колона или израз:

// SELECT MAX(`price`) FROM `products` WHERE `active` = 1
$maxPrice = $products->where('active', true)
	->max('price');

sum (string $expr): int

Връща сумата на стойностите в посочената колона или израз:

// 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

Позволява изпълнението на всяка функция за агрегиране.

// Изчислява средната цена на продуктите в дадена категория
$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) над групирани редове), като втори аргумент посочваме функцията за обобщаване, която да се приложи към тези междинни резултати:

// Изчислява общата цена на продуктите в наличност за всяка категория, след което сумира тези цени
$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' указва функцията за агрегиране, която да се прилага към междинните резултати.

Вмъкване, актуализиране и изтриване

Nette Database Explorer опростява вмъкването, актуализирането и изтриването на данни. Всички споменати методи изхвърлят Nette\Database\DriverException в случай на грешка.

Selection::insert (iterable $data)static

Вмъква нови записи в дадена таблица.

Вмъкване на единичен запис:

Новият запис се предава като асоциативен масив или итерабилен обект (като ArrayHash, използван във формулярите), където ключовете съответстват на имената на колоните в таблицата.

Ако таблицата има дефиниран първичен ключ, методът връща обект ActiveRow, който се презарежда от базата данни, за да отрази всички промени, направени на ниво база данни (например тригери, стойности на колоните по подразбиране или изчисления за автоматично увеличаване). По този начин се осигурява последователност на данните и обектът винаги съдържа текущите данни от базата данни. Ако не е изрично дефиниран първичен ключ, методът връща входните данни като масив.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row е екземпляр на ActiveRow, който съдържа пълните данни на вмъкнатия ред,
// включително автоматично генерирания идентификатор и всички промени, направени от тригерите
echo $row->id;          // Извежда идентификатора на нововместения потребител
echo $row->created_at;  // Извежда времето на създаване, ако е зададено от тригер

Вмъкване на няколко записа наведнъж:

Методът insert() ви позволява да вмъкнете няколко записа с една SQL заявка. В този случай той връща броя на вмъкнатите редове.

$insertedRows = $explorer->table('users')->insert([
	[
		'name' => 'John',
		'year' => 1994,
	],
	[
		'name' => 'Jack',
		'year' => 1995,
	],
]);
// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995)
// $insertedRows ще бъде 2

Можете също така да предадете като параметър обект Selection с избрани данни.

$newUsers = $explorer->table('potential_users')
	->where('approved', 1)
	->select('name, email');

$insertedRows = $explorer->table('users')->insert($newUsers);

Вмъкване на специални стойности:

Стойностите могат да включват файлове, обекти DateTime или SQL литерали:

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // конвертира във формат на база данни
	'avatar' => fopen('image.jpg', 'rb'),   // вмъква съдържанието на двоичен файл
	'uuid' => $explorer::literal('UUID()'), // извиква функцията UUID()
]);

Selection::update (iterable $data)int

Обновява редове в таблица въз основа на зададен филтър. Връща броя на действително модифицираните редове.

Колоните, които трябва да бъдат актуализирани, се предават като асоциативен масив или итерабилен обект (като ArrayHash, използван във формуляри), където ключовете съответстват на имената на колоните в таблицата:

$affected = $explorer->table('users')
	->where('id', 10)
	->update([
		'name' => 'John Smith',
		'year' => 1994,
	]);
// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10

За промяна на числови стойности можете да използвате операторите += и -=:

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // увеличава стойността на колоната "точки" с 1
		'coins-=' => 1,   // намалява стойността на колоната "монети" с 1
	]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10

Selection::delete(): int

Изтрива редове от таблица въз основа на зададен филтър. Връща броя на изтритите редове.

$count = $explorer->table('users')
	->where('id', 10)
	->delete();
// DELETE FROM `users` WHERE `id` = 10

Когато извиквате update() или delete(), не забравяйте да използвате where(), за да посочите редовете, които трябва да бъдат актуализирани или изтрити. Ако не се използва where(), операцията ще бъде извършена върху цялата таблица!

ActiveRow::update (iterable $data)bool

Актуализира данните в ред от базата данни, представен от обекта ActiveRow. Той приема като параметър итерабилни данни, в които ключовете са имена на колони. За промяна на числови стойности можете да използвате операторите += и -=:

След извършване на актуализацията обектът ActiveRow автоматично се презарежда от базата данни, за да отрази всички промени, направени на ниво база данни (например тригери). Методът връща true само ако е настъпила реална промяна в данните.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // увеличава броя на прегледите
]);
echo $article->views; // Извеждане на текущия брой изгледи

Този метод актуализира само един конкретен ред в базата данни. За групови актуализации на множество редове използвайте метода Selection::update().

ActiveRow::delete()

Изтрива ред от базата данни, който е представен от обекта ActiveRow.

$book = $explorer->table('book')->get(1);
$book->delete(); // Изтрива книгата с ID 1

Този метод изтрива само един конкретен ред в базата данни. За групово изтриване на множество редове използвайте метода Selection::delete().

Връзки между таблиците

В релационните бази данни данните се разделят на няколко таблици и се свързват чрез външни ключове. Nette Database Explorer предлага революционен начин за работа с тези връзки – без да се пишат JOIN заявки или да се изисква каквато и да е конфигурация или генериране на същности.

За демонстрация ще използваме примерната база данни(налична в GitHub). Базата данни включва следните таблици:

  • author – автори и преводачи (колони id, name, web, born)
  • book – книги (колони id, author_id, translator_id, title, sequel_id)
  • tag – тагове (колони id, name)
  • book_tag – таблица с връзки между книги и тагове (колони book_id, tag_id)

Структура на базата данни

В този пример за база данни с книги откриваме няколко вида връзки (опростени в сравнение с действителността):

  • Всяка книга има един** автор; един автор може да напише множество книги.
  • Нещо към много (0:N) – Една книга може да има преводач; един преводач може да превежда **многобройни книги.
  • Нещо към едно (0:1) – Една книга може да има продължение.
  • Много-към-много (M:N) – Една книга може да има няколко тага, а един таг може да бъде присвоен на няколко книги.

При тези връзки винаги има родителска таблица и детска таблица. Например във връзката между автори и книги таблицата author е родител, а таблицата book е дете – можете да си представите, че книгата винаги “принадлежи” на автор. Това е отразено и в структурата на базата данни: таблицата “дете” book съдържа външен ключ author_id, който препраща към таблицата “родител” author.

Ако искаме да показваме книгите заедно с имената на техните автори, имаме две възможности. Или извличаме данните, като използваме една SQL заявка с JOIN:

SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id;

Или извличаме данните на два етапа – първо книгите, а след това техните автори – и ги обединяваме в PHP:

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books

Вторият подход е изненадващо по-ефективен. Данните се извличат само веднъж и могат да се използват по-добре в кеша. Точно по този начин работи Nette Database Explorer – той се справя с всичко под капака и ви предоставя чист API:

$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 – името на свойството съвпада с името на колоната на външния ключ без суфикса id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // намира автора чрез колоната 'author_id'.
echo $book->translator?->name; // намира преводача чрез колоната "translator_id".

При достъп до свойството $book->author Explorer търси колона в таблицата book, която съдържа низ author (т.е. author_id). Въз основа на стойността в тази колона той извлича съответния запис от таблицата author и го връща като обект ActiveRow. По подобен начин $book->translator използва колоната translator_id. Тъй като колоната translator_id може да съдържа null, се използва операторът ?->.

Алтернативен подход се предлага от метода ref(), който приема два аргумента – името на целевата таблица и свързващата колона – и връща екземпляр на ActiveRow или null:

echo $book->ref('author', 'author_id')->name;      // връзка към автора
echo $book->ref('author', 'translator_id')->name;  // връзка към преводача

Методът ref() е полезен, ако не може да се използва достъп, базиран на свойства, например когато таблицата съдържа колона със същото име като това на свойството (author). В други случаи се препоръчва използването на достъп, базиран на свойства, за по-добра четливост.

Explorer автоматично оптимизира заявките към базата данни. При итерация през книги и достъп до свързаните с тях записи (автори, преводачи) Explorer не генерира заявка за всяка книга поотделно. Вместо това той изпълнява само една SELECT заявка за всеки тип връзка, което значително намалява натоварването на базата данни. Например:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo $book->title . ': ';
	echo $book->author->name;
	echo $book->translator?->name;
}

Този код ще изпълни само три оптимизирани заявки към базата данни:

SELECT * FROM `book`;
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

Логиката за идентифициране на свързващата колона се определя от изпълнението на Conventions. Препоръчваме ви да използвате DiscoveredConventions, която анализира чуждите ключове и ви позволява да работите безпроблемно със съществуващите връзки между таблиците.

Достъп до подчинената таблица

Достъпът до подчинената таблица работи в обратна посока. Сега питаме кои книги е написал този автор или кои книги е превел този преводач. За този тип запитване използваме метода related(), който връща обект Selection със свързани записи. Ето един пример:

$author = $explorer->table('author')->get(1);

// Извеждане на всички книги, написани от автора
foreach ($author->related('book.author_id') as $book) {
	echo "Wrote: $book->title";
}

// Извеждане на всички книги, преведени от автора
foreach ($author->related('book.translator_id') as $book) {
	echo "Translated: $book->title";
}

Методът related() приема описанието на връзката като единичен аргумент, използвайки точкова нотация, или като два отделни аргумента:

$author->related('book.translator_id');    // единичен аргумент
$author->related('book', 'translator_id'); // два аргумента

Explorer може автоматично да открие правилната свързваща колона въз основа на името на родителската таблица. В този случай той свързва чрез колоната book.author_id, тъй като името на изходната таблица е author:

$author->related('book'); // използва book.author_id

Ако съществуват няколко възможни връзки, Explorer ще хвърли изключение AmbiguousReferenceKeyException.

Разбира се, можем да използваме метода related() и при итерация през множество записи в цикъл, като Explorer автоматично ще оптимизира заявките и в този случай:

$authors = $explorer->table('author');
foreach ($authors as $author) {
	echo $author->name . ' wrote:';
	foreach ($author->related('book') as $book) {
		echo $book->title;
	}
}

Този код генерира само две ефективни SQL заявки:

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors

Връзка "много към много

За връзка “много към много” (M:N) е необходима свързваща таблица (в нашия случай book_tag). Тази таблица съдържа две колони с външни ключове (book_id, tag_id). Всяка колона препраща към първичния ключ на една от свързаните таблици. За да извлечем свързани данни, първо извличаме записи от таблицата за връзка, като използваме related('book_tag'), а след това продължаваме към целевите данни:

$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 заявките в ефективна форма:

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

В методите where(), select(), order(), и group(), можете да използвате специални обозначения за достъп до колони от други таблици. Explorer автоматично създава необходимите JOIN-ове.

Записът Dot (parent_table.column) се използва за връзки 1:N, гледани от гледна точка на родителската таблица:

$books = $explorer->table('book');

// Намира книги, чиито имена на автори започват с 'Jon'
$books->where('author.name LIKE ?', 'Jon%');

// Подрежда книгите по името на автора в низходящ ред
$books->order('author.name DESC');

// Извежда заглавието на книгата и името на автора
$books->select('book.title, author.name');

Записът с колонки се използва за връзки 1:N от гледна точка на родителската таблица:

$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.

Свързващата колона може да бъде изрично посочена в скоби:

// Намира автори, превели книга с 'PHP' в заглавието
$authors->where(':book(translator).title LIKE ?', '%PHP%');

Нотациите могат да бъдат верижно свързани, за да се получи достъп до данни в няколко таблици:

// Намира автори на книги с етикет 'PHP'
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Разширяване на условията за JOIN

Методът joinWhere() добавя допълнителни условия за обединяване на таблици в SQL след ключовата дума ON.

Например, да кажем, че искаме да намерим книги, преведени от определен преводач:

// Намира книги, преведени от преводач с име '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-и могат да се дефинират псевдоними на таблици:

$tags = $explorer->table('tag')
	->joinWhere(':book_tag.book.author', 'book_author.born &lt; ?', 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

Ако не използвате контейнера Nette DI, можете да създадете инстанция на Nette\Database\Explorer ръчно:

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);
версия: 4.0