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, &$numOfPages = null) |
Задаёт постраничный вывод |
group($columns, ...$params) |
Группирует строки (GROUP BY) |
having($condition, ...$params) |
Добавляет условие HAVING для фильтрации сгруппированных строк |
Методы можно объединять в цепочку (так называемый текучий
интерфейс): $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) и при необходимости позволяет задать смещение:
$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: $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})");
// ['First Book (John Novak)', ...]
// Callback может вернуть и массив с парой ключ и значение:
$titles = $explorer->table('book')
->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['First Book' => 'John Novak', ...]
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): mixed
Возвращает сумму значений в указанном столбце или выражении:
// 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') складываем эти
промежуточные суммы category_total. Второй аргумент 'SUM'
указывает, что к промежуточным результатам нужно применить
функцию SUM.
Вставка, изменение и удаление
Nette Database Explorer упрощает вставку, изменение и удаление данных. Все
упомянутые методы в случае ошибки выбрасывают
Nette\Database\DriverException.
Selection::insert (iterable $data)
Вставляет в таблицу новые записи.
Вставка одной записи:
Новую запись передайте ассоциативным массивом или итерируемым
объектом (например, ArrayHash, который используется в формах), где ключи соответствуют именам
столбцов таблицы.
Если у таблицы задан первичный ключ, метод возвращает объект
ActiveRow, который перезагружается из базы данных, чтобы отразить
изменения, сделанные на уровне базы (триггеры, значения столбцов по
умолчанию, вычисление автоинкрементных столбцов). Тем самым
обеспечивается согласованность данных, а объект всегда содержит
актуальные данные из базы. Если у таблицы нет первичного ключа,
идентифицировать строку невозможно, и метод возвращает null.
$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
Изменяет строки в таблице согласно заданному фильтру. Возвращает количество действительно изменённых строк.
Изменяемые столбцы передайте ассоциативным массивом или
итерируемым объектом (например, 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. Принимает итерируемую структуру с данными для изменения
(ключи – имена столбцов). Для изменения числовых значений можно
использовать операторы += и -=:
После выполнения изменения ActiveRow автоматически
перезагружается из базы данных, чтобы отразить изменения, сделанные на
уровне базы (например, триггерами). Метод возвращает true только
тогда, когда данные действительно изменились.
$article = $explorer->table('article')->get(1);
$article->update([
'views += 1', // увеличивает счётчик просмотров
]);
echo $article->views; // Выведет текущее количество просмотров
Этот метод изменяет только одну конкретную строку в базе данных. Для массового изменения нескольких строк используйте метод Selection::update().
ActiveRow::delete(): int
Удаляет строку базы данных, представленную объектом ActiveRow.
Возвращает количество удалённых строк, которое должно быть равно 1.
$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, в коде мы используем
nullsafe-оператор ?->.
Другой подход предлагает метод 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 при соединении таблиц.