Отладка 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, нагенерируем кучу товаров с категориями и выведем их на фронтенде.

7 комментариев

Александр Наумов
// Через точку можно указывать вложенные ключи в массиве!
{key: 'category.title', label: this.$t('models.products.category')},
У меня магия так и не сработала, так и не вывелось название категории:
Перепробовал различные варианты. Не понял, почему category.title, а не например categories.title - это хотя бы название привязанной таблицы. Магия с category_id у меня не завелась.
Незначительные опечатки:
src/admin/products.vue
должно быть:
src/admin/pages/products.vue
{key: 'category.title', label: this.$t('models.products.category')}
должно быть:
{key: 'category.title', label: this.$t('models.product.category')}
Александр Наумов
Разобрался, откуда данные должны подтягиваться в category.title - берутся из json. Но все равно Категория без данных.
Александр Наумов
Посмотрел через DevTools откуда подгружается json и он грузится по ссылке: http://vesp-shop.test/api/admin/products где отсутствует category вот в этом и проблема:
а по ссылке http://vesp-shop.test/api/web/products все в порядке:
Осталось понять как формируется json?
Василий Наумкин
Все контроллеры Vesp выдают JSON по умолчанию, а результат формируется из условий запроса.
Думаю, ты поторопился и пропустил в этой заметке раздел с выборкой категории, где рассказано, что в контроллере core/src/Controllers/Admin/Products.php должна быть вот такая функция:
protected function afterCount(Builder $c): Builder
{
    // Присоединение 2х колонок категории товара
    $c->with('category:id,title');

    return $c;
}
Немного доработал заметку, чтобы было понятнее.
Александр Наумов
Василий, спасибо большое, за такие развернутые ответы! Как всегда у тебя, все классно запроектировано, спасибо, что делишься опытом - узнаю много нового!
Василий Наумкин
Спасибо, что читаешь!
Василий Наумкин
Не понял, почему category.title, а не например categories.title - это хотя бы название привязанной таблицы. Магия с category_id у меня не завелась.
Потому что мы используем название связи, а не привязанной таблицы. В модели Product прописан метод category() - вот она и используется для работы.
public function category(): BelongsTo
{
    return $this->belongsTo(Category::class);
}
Связь указывается между моделями, а в какой таблице модель хранит данные никого не интересует.
Незначительные опечатки:
Спасибо, поправил!
bezumkin.ru
Personal website of Vasily Naumkin
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Futuris
04.04.2024, 08:56:12
Я просто немного запутался. Когда в абзаце "Vesp/Core" ты пишешь про "новый trait FileModel", я поду...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!