Отладка SQL запросов контроллеров

Возможно вы обратили внимание, что на прошлом уроке мы добавили в таблицы категорий и товаров строку поиска, но она не работает?

Это потому, что никто не написал сам функционал поиска в контроллерах. Мы просто расширили базовый ModelController и больше ничего не меняли.

Помимо этого в категориях мы указали колонку для вывода общего количества товаров - и она тоже пуста, по той же причине.

Предлагаю сегодня доработать наши контроллеры и добавить недостающие функции.

Отладка SQL запросов

В качестве dev-зависимости Vesp устанавливает очень полезный пакет для отладки запросов - Clockwork, вам нужно только установить его расширение для браузера.

И вот, что мы сейчас видим при обычном выводе таблицы категорий

Мы видим всего 3 запроса:

  • загрузку группы пользователя для проверки его разрешений
  • подсчёт общего количества результатов
  • и собственно выборка данных с заданным лимитом

Куда же делась загрузка самого пользователя и его токена, спросите вы? Они не выводятся по умолчанию, потому что слежение Clockwork включено только для маршрутов, а пользователь загружается перед контроллером, в middleware.

Но если хотите, можно изменить файл core/routes.php и заменить

  • $group->add(Vesp\Middlewares\Clockwork::class);
  • на $app->add(Vesp\Middlewares\Clockwork::class);

Тогда вы будете видеть вообще все запросы:

Вот теперь, давайте посмотрим, что происходит во время изменения строки поиска? Правильно, запросы уходят на сервер, но результаты не меняется - ведь наш контроллер их просто не обрабатывает.

Поиск в котроллере

Так как поиск меняет количество результатов, мы должны обрабатывать его в методе beforeCount, чтобы выдавалось верное общее количество результатов:

protected function beforeCount(Builder $c): Builder
{
    // Если указан параметр query
    if ($query = $this->getProperty('query')) {
        // Делаем вложенный запрос
        $c->where(static function (Builder $c) use ($query) {
            // В котором ищем по title
            $c->where('title', 'LIKE', "%$query%");
            // Или description
            $c->orWhere('description', 'LIKE', "%$query%");
        });
    }

    return $c;
}

Как видите, вложенный запрос помещается в скобки, так что если вы добавите потом еще одно отдельное условие, например $c->where('active', true); всё будет работать корректно.

Вывод количества товаров

С поиском понятно, а как выводить количество товаров, привязанных к категории? Тут Eloquent предлагает очень удобную функцию withCount(), которы мы используем в : core/src/Controllers/Admin/Categories.php:

protected function afterCount(Builder $c): Builder
{
    // Подсчёт количества товаров категории
    $c->withCount('products');

    return $c;
}

withCount() требует указать имя связанных моделей, а мы прописывали товары категории в модели Product, помните? Вот через эту связь всё и работает.

Вывод категории товара

Давайте теперь примерно так же выведем и категорию у товара, тоже через связь. Для этого используется следующая функция в котроллере core/src/Controllers/Admin/Products.php:

protected function afterCount(Builder $c): Builder
{
    // Присоединение 2х колонок категории товара
    $c->with('category:id,title');

    return $c;
}

Смотрим на запрос и видим, что добавилась выборка категорий с определёнными id.

Eloquent не очень любит использовать join таблиц и вместо этого предлагает довольно хитрую штуку - он сначала выбирает наши товары, потом смотрит в их category_id, выбирает отдельным запросом только нужные категории и затем проходит циклом по товарам, добавляя в них родительскую категорию.

И вот, что мы видим в ответе - вложенный массив с категорией, который содержит всего 2 запрошенных колонки:

Согласитесь, очень кратко и удобно. Главное, не забывать просписывать связи в моделях и выбирать соответствующие колонки в запросе. Если бы я не указал id в списке колонок категории, Eloquent бы не знал, к каким товарам её присоединить.

Вообще, таких присоединений можно делать довольно много, и выборки не начинают тормозить при большом количестве результатов, в отличие от join. Минус только один - по таким таблицам нельзя делать сортировку, для этого необходим всё-таки join.

Так что, если нужно просто выбрать данные - используем with(), если нужны и данные и сортировка - то join().

Теперь осталось только вывести новую колонку в таблице с товарами в админке. Редактируем src/admin/pages/products.vue

fields() {
  return [
    {key: 'id', label: this.$t('components.table.columns.id'), sortable: true},
    {key: 'sku', label: this.$t('models.product.sku'), sortable: true},
    // Через точку можно указывать вложенные ключи в массиве!
    {key: 'category.title', label: this.$t('models.product.category')},
    // ...

Пересобираем фронт и любуемся на результат:

Заключение

Вот такой небольшой, но очень полезный урок по отладке наших контроллеров.

Теперь вы всегда будете видеть, как именно проходит запрос и почему контроллер возвращает не то, что вам нужно.

Все изменения можно посмотреть на Github. Я там нашёл и поправил опечатку с указанием даты изменения в таблице товаров.

На следующем уроке освоим библиотеку Faker, нагенерируем кучу товаров с категориями и выведем их на фронтенде.

← Предыдущая заметка
Начинаем разработку VespShop
Следующая заметка →
Выводим товары на сайте
Комментарии (7)
bezumkinВасилий Наумкин
06.07.2022 06:23

Все контроллеры Vesp выдают JSON по умолчанию, а результат формируется из условий запроса.

Думаю, ты поторопился и пропустил в этой заметке раздел с выборкой категории, где рассказано, что в контроллере core/src/Controllers/Admin/Products.php должна быть вот такая функция:

protected function afterCount(Builder $c): Builder
{
    // Присоединение 2х колонок категории товара
    $c->with('category:id,title');

    return $c;
}

Немного доработал заметку, чтобы было понятнее.

bezumkinВасилий Наумкин
06.07.2022 11:19

Спасибо, что читаешь!

bezumkinВасилий Наумкин
06.07.2022 06:30

Не понял, почему category.title, а не например categories.title - это хотя бы название привязанной таблицы. Магия с category_id у меня не завелась.

Потому что мы используем название связи, а не привязанной таблицы. В модели Product прописан метод category() - вот она и используется для работы.

public function category(): BelongsTo
{
    return $this->belongsTo(Category::class);
}

Связь указывается между моделями, а в какой таблице модель хранит данные никого не интересует.

Незначительные опечатки:

Спасибо, поправил!

bezumkin
Василий Наумкин
09.04.2024 01:45
Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. Во...
futuris
Futuris
04.04.2024 05:56
Я просто немного запутался. Когда в абзаце "Vesp/Core" ты пишешь про "новый trait Fil...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...