Проводник базы данных
Explorer предлагает интуитивно понятный и эффективный способ работы с базой данных. Он автоматически обрабатывает связи между таблицами, создает оптимизированные запросы и позволяет вам сосредоточиться на логике приложения. Конфигурация не требуется. Для полного контроля вы можете перейти на SQL.
- Работа с данными становится естественной и понятной
- Генерирует оптимизированные SQL-запросы, которые извлекают только необходимые данные
- Обеспечивает легкий доступ к связанным данным без необходимости написания JOIN-запросов
- Работает сразу, без какой-либо настройки или создания сущностей
Работа с проводником начинается с вызова метода table()
на
объекте Nette\Database\Explorer
(подробности о настройке соединения с базой данных см. в разделе Подключение и конфигурация ):
Метод возвращает объект Selection, который
представляет собой SQL-запрос. К этому объекту можно подключить
дополнительные методы для фильтрации и сортировки результатов. Запрос
собирается и выполняется только при запросе данных, например, при
итерации с помощью foreach
. Каждая строка представлена объектом ActiveRow:
Explorer значительно упрощает работу с отношениями таблиц. Следующий пример показывает, как легко мы можем вывести данные из связанных таблиц (книги и их авторы). Обратите внимание, что никаких JOIN-запросов писать не нужно – Nette генерирует их за нас:
Nette Database Explorer оптимизирует запросы для достижения максимальной эффективности. В приведенном выше примере выполняется всего два запроса SELECT, независимо от того, обрабатываем ли мы 10 или 10 000 книг.
Кроме того, Explorer отслеживает, какие столбцы используются в коде, и извлекает из базы данных только их, что еще больше снижает производительность. Это поведение полностью автоматическое и адаптивное. Если впоследствии вы измените код, чтобы использовать дополнительные столбцы, Explorer автоматически скорректирует запросы. Вам не нужно ничего настраивать или думать о том, какие столбцы будут нужны – предоставьте это Nette.
Фильтрация и сортировка
Класс Selection
предоставляет методы для фильтрации и сортировки
данных.
where($condition, ...$params) |
Добавляет условие WHERE. Несколько условий объединяются с помощью AND |
whereOr(array $conditions) |
Добавляет группу условий WHERE, объединенных с помощью OR |
wherePrimary($value) |
Добавляет условие ГДЕ на основе первичного ключа |
order($columns, ...$params) |
Устанавливает сортировку с помощью ORDER BY |
select($columns, ...$params) |
Указывает, какие столбцы следует извлечь |
limit($limit, $offset = null) |
Ограничивает количество строк (LIMIT) и опционально устанавливает OFFSET |
page($page, $itemsPerPage, &$total = null) |
Устанавливает пагинацию |
group($columns, ...$params) |
Группирует строки (GROUP BY) |
having($condition, ...$params) |
Добавляет условие HAVING для фильтрации сгруппированных строк |
Методы могут быть объединены в цепочку (так называемый fluent-интерфейс):
$table->where(...)->order(...)->limit(...)
.
Эти методы также позволяют использовать специальные обозначения для доступа к данным из связанных таблиц.
Эскейпинг и идентификаторы
Методы автоматически экранируют параметры и заключают в кавычки идентификаторы (имена таблиц и столбцов), предотвращая SQL-инъекции. Чтобы обеспечить правильную работу, необходимо соблюдать несколько правил:
- Записывайте ключевые слова, имена функций, процедур и т. д. в верхнем регистре.
- Имена столбцов и таблиц пишите в строчном регистре.
- Всегда передавайте строки с помощью параметров.
where(string|array $condition, …$parameters): static
Фильтрует результаты с помощью условий WHERE. Его сильной стороной является интеллектуальная обработка различных типов значений и автоматический выбор операторов SQL.
Базовое использование:
Благодаря автоматическому определению подходящих операторов вам не нужно разбираться с особыми случаями – Nette сделает это за вас:
Метод также корректно обрабатывает отрицательные условия и пустые массивы:
Вы также можете передать результат другого запроса к таблице в качестве параметра, создав подзапрос:
Условия также можно передать в виде массива, объединив элементы с помощью AND:
В массиве можно использовать пары ключ-значение, и Nette снова автоматически выберет нужные операторы:
Мы также можем смешивать SQL-выражения с заполнителями и несколькими параметрами. Это полезно для сложных условий с точно определенными операторами:
Несколько вызовов where()
автоматически объединяют условия с
помощью AND.
whereOr(array $parameters): static
Аналогично where()
, но объединяет условия с помощью OR:
Можно использовать и более сложные выражения:
wherePrimary(mixed $key): static
Добавляет условие для первичного ключа таблицы:
Если таблица имеет составной первичный ключ (например, foo_id
,
bar_id
), мы передаем его в виде массива:
order(string $columns, …$parameters): static
Указывает порядок, в котором возвращаются строки. Вы можете сортировать по одному или нескольким столбцам, по возрастанию или убыванию, или по пользовательскому выражению:
select(string $columns, …$parameters): static
Указывает столбцы, которые будут возвращены из базы данных. По
умолчанию Nette Database Explorer возвращает только те столбцы, которые
действительно используются в коде. Используйте метод select()
, если
вам нужно получить конкретные выражения:
Псевдонимы, определенные с помощью AS
, становятся доступны как
свойства объекта ActiveRow
:
limit(?int $limit, ?int $offset = null): static
Ограничивает количество возвращаемых строк (LIMIT) и опционально задает смещение:
Для пагинации целесообразнее использовать метод page()
.
page(int $page, int $itemsPerPage, &$numOfPages = null): static
Упрощает пагинацию результатов. Принимает номер страницы (начиная с 1) и количество элементов на странице. В качестве опции можно передать ссылку на переменную, в которой будет храниться общее количество страниц:
group(string $columns, …$parameters): static
Группирует строки по указанным столбцам (GROUP BY). Обычно используется в сочетании с агрегатными функциями:
having(string $having, …$parameters): static
Задает условие для фильтрации сгруппированных строк (HAVING). Может
использоваться в сочетании с методом group()
и агрегатными
функциями:
Чтение данных
Для чтения данных из базы данных существует несколько полезных методов:
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-запрос.
get($key): ?ActiveRow
Выполняет SQL-запрос и возвращает строку по первичному ключу или
null
, если он не существует.
fetch(): ?ActiveRow
Возвращает одну строку и переводит внутренний указатель на
следующую. Если строк больше нет, возвращается null
.
fetchPairs(string|int|null $key = null, string|int|null $value = null): array
Возвращает результаты в виде ассоциативного массива. В первом аргументе указывается имя столбца, который будет использоваться в качестве ключа массива, а во втором – имя столбца, который будет использоваться в качестве значения:
Если указан только первый параметр, то в качестве значения будет
использован весь ряд, представленный в виде объекта ActiveRow
:
В случае дублирования ключей используется значение из последней
строки. При использовании null
в качестве ключа массив будет
проиндексирован по нулям (в этом случае коллизий не возникает):
fetchPairs(Closure $callback): array
В качестве альтернативы вы можете передать в качестве параметра обратный вызов. Обратный вызов будет применен к каждому ряду, чтобы вернуть либо одиночное значение, либо пару ключ-значение.
fetchAll(): array
Возвращает все строки в виде ассоциативного массива объектов
ActiveRow
, где ключами являются значения первичного ключа.
count(): int
Метод count()
без параметров возвращает количество строк в
объекте Selection
:
Примечание: count()
с параметром выполняет функцию агрегирования
COUNT в базе данных, как описано ниже.
ActiveRow::toArray(): array
Преобразует объект ActiveRow
в ассоциативный массив, ключами
которого являются имена столбцов, а значениями – соответствующие
данные.
Агрегация
Класс 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() без параметра просто
возвращает количество строк в объекте Selection
.
min(string $expr) and max(string $expr)
Методы min()
и max()
возвращают минимальное и максимальное
значения в указанном столбце или выражении:
sum(string $expr): int
Возвращает сумму значений в указанном столбце или выражении:
aggregation(string $function, ?string $groupFunction = null): mixed
Позволяет выполнить любую агрегатную функцию.
Если нам нужно агрегировать результаты, которые сами являются
результатом агрегирования и группировки (например, SUM(value)
над
сгруппированными строками), то в качестве второго аргумента мы
указываем функцию агрегирования, которая будет применяться к этим
промежуточным результатам:
В этом примере мы сначала вычисляем общую цену товаров в каждой
категории (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
, который перезагружается из базы данных, чтобы
отразить любые изменения, сделанные на уровне базы данных (например,
триггеры, значения столбцов по умолчанию или вычисления с
автоинкрементами). Это обеспечивает согласованность данных, и объект
всегда содержит текущие данные базы данных. Если первичный ключ не
определен явно, метод возвращает входные данные в виде массива.
Вставка нескольких записей одновременно:.
Метод insert()
позволяет вставить несколько записей с помощью
одного SQL-запроса. В этом случае он возвращает количество
вставленных строк.
В качестве параметра можно также передать объект Selection
с
выборкой данных.
Вставка специальных значений:
Значения могут включать файлы, объекты DateTime
или литералы SQL:
Selection::update(iterable $data): int
Обновляет строки в таблице на основе заданного фильтра. Возвращает количество фактически измененных строк.
Обновляемые столбцы передаются в виде ассоциативного массива или
итерируемого объекта (например, ArrayHash
, используемого в формах), где ключи соответствуют именам столбцов
в таблице:
Для изменения числовых значений можно использовать операторы
+=
и -=
:
Selection::delete(): int
Удаляет строки из таблицы на основе заданного фильтра. Возвращает количество удаленных строк.
При вызове update()
или delete()
обязательно
используйте where()
для указания обновляемых или удаляемых строк.
Если where()
не используется, операция будет выполнена над всей
таблицей!
ActiveRow::update(iterable $data): bool
Обновляет данные в строке базы данных, представленной объектом
ActiveRow
. В качестве параметра он принимает итерируемые данные, где
ключами являются имена столбцов. Для изменения числовых значений
можно использовать операторы +=
и -=
:
После выполнения обновления ActiveRow
автоматически
перезагружается из базы данных, чтобы отразить все изменения,
сделанные на уровне базы данных (например, триггерами). Метод
возвращает true
только в том случае, если произошло реальное
изменение данных.
Этот метод обновляет только одну конкретную строку в базе данных. Для массового обновления нескольких строк используйте метод Selection::update().
ActiveRow::delete()
Удаляет из базы данных строку, представленную объектом
ActiveRow
.
Этот метод удаляет только один конкретный ряд в базе данных. Для массового удаления нескольких строк используйте метод Selection::delete().
Отношения между таблицами
В реляционных базах данных данные разделены на несколько таблиц и связаны между собой с помощью внешних ключей. Nette Database Explorer предлагает революционный способ работы с этими отношениями – без написания запросов JOIN и без необходимости конфигурирования или генерации сущностей.
Для демонстрации мы воспользуемся базой данных example(доступна на 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) – У книги может быть переводчик; переводчик может перевести множество книг.
- Zero-to-one (0:1) – Книга может иметь продолжение.
- Много-ко-многим (M:N) – Книга может иметь несколько тегов, и один тег может быть присвоен нескольким книгам.
В этих отношениях всегда есть родительская таблица и детская
таблица. Например, в отношениях между авторами и книгами таблица
author
является родительской, а таблица book
– дочерней –
можно считать, что книга всегда „принадлежит“ одному автору. Это
также отражено в структуре базы данных: дочерняя таблица book
содержит внешний ключ author_id
, который ссылается на родительскую
таблицу author
.
Если мы хотим отобразить книги вместе с именами их авторов, у нас есть два варианта. Либо мы получаем данные с помощью одного SQL-запроса с JOIN:
Либо мы получаем данные в два этапа – сначала книги, затем их авторов – и собираем их в PHP:
Второй подход, как ни странно, более эффективен. Данные извлекаются только один раз и могут быть лучше использованы в кэше. Именно так работает Nette Database Explorer – он обрабатывает все под капотом и предоставляет вам чистый API:
Доступ к родительской таблице
Доступ к родительской таблице очень прост. Это такие отношения, как
у книги есть автор или у книги может быть переводчик. Доступ к
связанной записи можно получить через свойство объекта ActiveRow
–
имя свойства совпадает с именем столбца внешнего ключа без суффикса
id
:
При обращении к свойству $book->author
Explorer ищет в таблице
book
столбец, содержащий строку author
(например,
author_id
). На основании значения в этом столбце он извлекает
соответствующую запись из таблицы author
и возвращает ее в виде
объекта ActiveRow
. Аналогично, $book->translator
использует столбец
translator_id
. Поскольку столбец translator_id
может содержать
null
, используется оператор ?->
.
Альтернативный подход обеспечивается методом ref()
, который
принимает два аргумента – имя целевой таблицы и связывающий
столбец – и возвращает экземпляр ActiveRow
или null
:
Метод ref()
полезен, если доступ на основе свойств не может быть
использован, например, когда таблица содержит столбец с тем же именем,
что и свойство (author
). В других случаях рекомендуется
использовать доступ на основе свойств для лучшей читабельности.
Explorer автоматически оптимизирует запросы к базе данных. При итерации книг и доступе к связанным с ними записям (авторы, переводчики) Explorer не генерирует запрос для каждой книги в отдельности. Вместо этого он выполняет только один запрос SELECT для каждого типа отношений, что значительно снижает нагрузку на базу данных. Например:
Этот код выполнит только три оптимизированных запроса к базе данных:
Логика определения связующего столбца определяется реализацией Conventions. Мы рекомендуем использовать DiscoveredConventions, которая анализирует внешние ключи и позволяет беспрепятственно работать с существующими связями таблиц.
Доступ к дочерней таблице
Доступ к дочерней таблице работает в обратном направлении. Теперь мы
спрашиваем какие книги написал этот автор или какие книги
перевел этот переводчик. Для этого типа запроса мы используем метод
related()
, который возвращает объект Selection
с
соответствующими записями. Вот пример:
Метод related()
принимает описание отношения как один аргумент с
использованием точечной нотации или как два отдельных аргумента:
Explorer может автоматически определить правильный столбец связи на
основе имени родительской таблицы. В данном случае связь
устанавливается через столбец book.author_id
, поскольку имя исходной
таблицы – author
:
Если существует несколько возможных связей, Explorer выбросит исключение AmbiguousReferenceKeyException.
Конечно, мы также можем использовать метод related()
при
циклическом переборе нескольких записей, и Explorer автоматически
оптимизирует запросы и в этом случае:
Этот код генерирует только два эффективных SQL-запроса:
Отношения "многие-ко-многим
Для отношений „многие-ко-многим“ (M:N) требуется
таблица-перекресток (в нашем случае book_tag
). Эта таблица
содержит два столбца с внешними ключами (book_id
, tag_id
).
Каждый столбец ссылается на первичный ключ одной из связанных таблиц.
Чтобы получить связанные данные, мы сначала извлекаем записи из
таблицы связей с помощью related('book_tag')
, а затем переходим к целевым
данным:
Explorer снова оптимизирует SQL-запросы в эффективную форму:
Запрос через связанные таблицы
В методах where()
, select()
, order()
и group()
можно
использовать специальные обозначения для доступа к столбцам из других
таблиц. Explorer автоматически создает необходимые JOIN.
Точечная нотация (parent_table.column
) используется для отношений
1:N с точки зрения родительской таблицы:
Точечная нотация используется для отношений 1:N с точки зрения родительской таблицы:
В приведенном выше примере с обозначением двоеточия (:book.title
)
столбец внешнего ключа явно не указан. Explorer автоматически определяет
нужный столбец на основе имени родительской таблицы. В данном случае
соединение выполняется через столбец book.author_id
, поскольку имя
исходной таблицы – author
. Если существует несколько возможных
соединений, Explorer выбрасывает исключение AmbiguousReferenceKeyException.
Связывающий столбец можно явно указать в круглых скобках:
Нотации можно объединять в цепочки для доступа к данным в нескольких таблицах:
Расширение условий для JOIN
Метод joinWhere()
добавляет дополнительные условия к объединению
таблиц в SQL после ключевого слова ON
.
Например, мы хотим найти книги, переведенные определенным переводчиком:
В условии joinWhere()
можно использовать те же конструкции, что и в
методе where()
, – операторы, заполнители, массивы значений или
выражения SQL.
Для более сложных запросов с несколькими JOIN можно определить псевдонимы таблиц:
Обратите внимание, что если метод where()
добавляет условия в
предложение WHERE
, то метод joinWhere()
расширяет условия в
предложении ON
при объединении таблиц.