Database Explorer

Explorer предлага интуитивен и ефективен начин за работа с базата данни. Той автоматично се грижи за релациите между таблиците и оптимизацията на заявките, така че можете да се съсредоточите върху своето приложение. Работи веднага без настройка. Ако се нуждаете от пълен контрол над SQL заявките, можете да използвате SQL достъп.

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

С Explorer започвате с извикване на метода 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->title;
	echo 'Автор: ' . $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 за филтриране на групирани редове

Методите могат да се навързват (т.нар. fluent interface): $table->where(...)->order(...)->limit(...).

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

Екраниране и идентификатори

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

  • Ключовите думи, имената на функции, процедури и т.н. пишете с главни букви.
  • Имената на колони и таблици пишете с малки букви.
  • Низовете винаги вмъквайте чрез параметри.
where('name = ' . $name);         // КРИТИЧНА УЯЗВИМОСТ: SQL injection
where('name LIKE "%search%"');    // ГРЕШНО: усложнява автоматичното ограждане с кавички
where('name LIKE ?', '%search%'); // ПРАВИЛНО: стойност, вмъкната чрез параметър

where('name like ?', $name);     // ГРЕШНО: генерира: `name` `like` ?
where('name LIKE ?', $name);     // ПРАВИЛНО: генерира: `name` LIKE ?
where('LOWER(name) = ?', $value);// ПРАВИЛНО: LOWER(`name`) = ?

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 < 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 (`age` > 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) и опционално позволява да се зададе offset:

$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 "Общо страници: $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

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

$titles = $explorer->table('book')
	->fetchPairs(fn($row) => "$row->title ({$row->author->name})");
// ['Първа книга (Ян Новак)', ...]

// Callback може също да връща масив с двойка ключ & стойност:
$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) и max(string $expr)

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

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

sum (string $expr)

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

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

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

// средна цена на продуктите в категория
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

// свързва етикетите на продукта в един низ
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

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

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

Insert, Update & Delete

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

Selection::insert (iterable $data)

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

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

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

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

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row е инстанция на ActiveRow и съдържа пълните данни на вмъкнатия ред,
// включително автоматично генерираното ID и евентуалните промени, извършени от тригери
echo $row->id; // Извежда 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

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

Променяните колони предаваме като асоциативен масив или iterable обект (например 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,  // увеличава стойността на колоната 'points' с 1
		'coins-=' => 1,   // намалява стойността на колоната 'coins' с 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. Като параметър приема iterable с данни, които трябва да се актуализират (ключовете са имената на колоните). За промяна на числови стойности можем да използваме операторите += и -=:

След извършване на актуализацията 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)
Структура на базата данни, използвана в примерите

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

  • Едно към много (1:N) – всяка книга има един автор, авторът може да напише няколко книги.
  • Нула към много (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);  -- id на авторите на получените книги

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

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'заглавие: ' . $book->title;
	echo 'написано от: ' . $book->author->name; // $book->author е запис от таблица 'author'
	echo 'преведено от: ' . $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)); -- id от колоната author_id на избраните книги
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- id от колоната translator_id на избраните книги

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

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

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

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

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

// Извежда всички книги, които авторът е превел
foreach ($author->related('book.translator_id') as $book) {
	echo "Превел: $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 . ' написал:';
	foreach ($author->related('book') as $book) {
		echo $book->title;
	}
}

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

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id на избраните автори

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

За връзка много към много (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, ...));  -- id на избраните книги
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- id на етикетите, намерени в book_tag

Заявки през свързани таблици

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

Точкова нотация (родителска_таблица.колона) се използва за връзка 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_id).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 < ?', 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 при свързване на таблици.

версия: 4.0