Эффективные Eloquent запросы в Laravel
Эффективный Eloquent в Laravel

Эффективный Eloquent в Laravel

Приготовьтесь повысить уровень своих навыков в Laravel с помощью этого руководства по Eloquent запросам! Вы узнаете все, что вам нужно знать, от начинающих до продвинутых техник.

Для начала давайте сделаем шаг назад и поговорим о том, что такое Eloquent. Eloquent - это объектно-реляционное отображение (ORM) для Laravel и конструктор запросов. Вы можете использовать ORM для работы с Eloquent моделями, чтобы быстро и эффективно опрашивать вашу базу данных. Вы также можете использовать конструктор запросов через интерфейс базы данных для создания своих запросов вручную.

О чем мы будем говорить сегодня? У меня есть приложение, над которым я работал для своего выступления в Laracon EU, это банковское приложение - захватывающее, я знаю. Но когда дело доходит до запросов в базу данных, бывают интересные кейсы.

Данные, с которыми мы работаем: у пользователя может быть много учетных записей, на каждой учетной записи хранится текущий баланс. Каждая учетная запись содержит множество транзакций, и транзакция связывает учетную запись с поставщиком вместе с суммой, на которую была произведена транзакция. Вы можете увидеть диаграмму SQL для этого ниже, если хотите увидеть визуальное представление.

В своем выступлении для Laracon я делаю что-то конкретное с запросами, поскольку оно фокусируется на самом API. Однако есть много других запросов, которые мы могли бы использовать, поэтому давайте пройдемся по ним.

Чтобы получить все учетные записи для пользователя, который вошел в систему, мы можем написать это довольно просто следующим образом:

use App\Models\Account;
$accounts = Account::where('user_id', auth()->id())->get();

Обычно мы записываем этот запрос в наш контроллер и возвращаем результаты. Я не буду вдаваться в детали результатов запросов в этом руководстве, а то статья может получится очень большой. Однако может быть несколько мест, где мы хотим выполнить один и тот же запрос. Например, если бы мы создавали внутреннюю панель администратора и хотели просмотреть учетные записи пользоватей от администратора.

Начнем с того, что моя личная ошибка заключается в том, что я не запускаю новый конструктор Eloquent для каждого запроса - хотя это так легко сделать. Это позволяет вам полностью упросить работу с IDE без необходимости установки и настройки каких-либо дополнительных плагинов. Чтобы сделать это, выполним первую часть нашего запроса статическим вызовом метода query.

use App\Models\Account;
$accounts = Account::query()->where('user_id', auth()->id())->get();

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

Вместо того, чтобы использовать ORM, мы могли бы использовать фасад DB для выполнения этого запроса - который, конечно, меньше использует память и будет выполнен немного быстрее. Вероятность того, что вы увидите разницу в скорости, очень мала, если у вас нет больших наборов данных. Давайте взглянем на этот запрос.


use Illuminate\Support\Facades\DB;
$accounts = DB::table('accounts')->where('user_id', auth()->id())->get();

В моих тестах фасад DB использовал гораздо меньше памяти, но это потому, что он возвращает коллекцию объектов. Принимая во внимание, что запрос ORM вернет коллекцию моделей, которые необходимо построить и сохранить в памяти. Такова плата за использование Eloquent модели.

Давайте двигаться дальше. В моем примере у меня есть контроллер, который выполняет этот запрос в строке и возвращает результаты. Я уже говорил, что этот запрос может быть повторно использован в других прикладных областях, так что я могу сделать, чтобы убедиться, что я могу контролировать эти запросы более глобально? Классы запросов спешат на помощь!

Это шаблон, который я использую довольно часто, и вам следует, по крайней мере, посмотреть на него, если вы не собираетесь его перенимать. Этому трюку я научился в мире CQRS, где операции чтения классифицируются как запросы, а операции записи - как команды. Что мне нравится в CQRS, так это его способность разделять логику между тем, о чем должен знать контроллер, и классом, предназначенным для простого запроса данных. Давайте взглянем на этот класс:

final class FetchAccountsForUser implements FetchAccountsForUserContract
{
    public function handle(string $user): Collection
    {
        return Account::query()
            ->where('user_id', $user)
            ->get();
    }
}

Это единый класс запросов, который выполняет только одну вещь, и в типичной для Стива манере он использует контракт / интерфейс, так что я могу перенести это в контейнер и разрешить запрос там, где мне нужно. Итак, теперь, в нашем контроллере, нам нужно только запустить следующее:

$accounts = $this->query->handle(
    user: auth()->id(),
);

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

Поэтому, часто запрашивая данные в своих приложениях, вы заметите, что запросы не настолько динамичны. Да, значения, которые вы хотите передать, будут динамическими, основанными на вводимых пользователями данных. Однако запросы будут меняться только иногда. Например, конечная точка API с опциями для включения связей, фильтрации, сортировки результатов и т.д.

Мы ввели новую проблему в наше приложение. Как мы можем поддерживать динамические и нединамические запросы в нашем приложении без использования разных рабочих процессов? До сих пор мы реорганизовали, чтобы использовать класс запросов, предназначенный для выполнения запроса и возврата нашего результата.

Можно передать конструктор запросов в класс query, что позволяет мне превратить динамическую часть того, что мне нужно, во что-то более статичное. Давайте посмотрим, как мы могли бы подойти к этому.

final class FetchTransactionForAccount implements FetchTransactionForAccountContract
{
    public function handle(Builder $query, string $account): Builder
    {
        return $query->where('account_id', $account);
    }
}

Тогда мы бы вызвали это внутри нашего контроллера следующим образом.

public function __invoke(Request $request, string $account): JsonResponse
{
    $transactions = $this->query->handle(
        query: Transaction::query(),
        account: $account,
    )->get();
}

Мы можем достичь этого, передав Transaction::query() в наш контроллер и идентификатор учетной записи. Класс query возвращает экземпляр query builder, поэтому нам нужно вернуть get в результате. Этот упрощенный пример, возможно, не очень хорошо иллюстрирует преимущества, поэтому я рассмотрю альтернативу.

Представьте, что у нас есть запрос, в котором мы всегда хотим возвращать выборку связей и областей применения (scopes). Например, мы хотим показать самые последние учетные записи пользователей с общим количеством транзакций.

$accounts = Account::query()
    ->withCount('transactions')
    ->whereHas('transactions', function (Builder $query) {
        $query->where(
            'created_at',
            now()->subDays(7),
        )
    })->latest()->get();

В одну строку, это разумный запрос. Но если у нас есть это в нескольких местах, и нам внезапно нужно начать расширять это, чтобы добавить дополнительные области или показывать только учетные записи, которые были активны в течение 30 дней... Вы можете себе представить, как быстро это может вырасти.

Давайте посмотрим, как это работает в подходе класса запросов:

final class RecentAccountsForUser implements RecentAccountsForUserContract
{
    public function handle(Builder $query, int $days = 7): Builder
    {
        $query
            ->withCount('transactions')
            ->whereHas('transactions', function (Builder $query) {
                $query->where(
                    'created_at',
                    now()->subDays($days),
                )
            });
    }
}

Когда мы приступим к реализации этого:

public function __invoke(Request $request): JsonResponse
{
    $accounts = $this->query->handle(
        query: Account::query()->where('user_id', auth()->id()),
    )->latest()->get();
 
    // handle the return.
}

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

Но нужно ли это? Я знаю, что многие люди просто добавили бы это к определенному методу внутри вашей модели, и это прекрасно. Но тогда мы будем увеличивать наши модели с каждым запросом на изменение этого, поскольку все мы знаем, что скорее добавим вспомогательный метод, чем заменим его. Подходя таким образом, вы оцениваете преимущества добавления этого по сравнению с расширением того, что у вас есть. Не успеете оглянуться, как в вашей модели появится 30 таких вспомогательных методов, которые необходимо добавлять к каждой модели, возвращаемой в коллекцию.

Что, если бы мы захотели перейти на использование фасада БД во всем нашем приложении? Внезапно у нас появляется много логики, которую нужно изменить во многих местах, и наши результаты становятся очень непредсказуемыми. Давайте посмотрим, как будет выглядеть этот запрос с использованием фасада DB.


$latestAccounts = DB::table(
    'transactions'
)->join(
    'accounts',
    'transactions.account_id', '=', 'accounts.id'
)->select(
    'accounts.*',
    DB::raw(
        'COUNT(transactions.id) as total_transactions')
)->groupBy(
    'transactions.account_id'
)->orderBy('transactions.created_at', 'desc')->get();

Как насчет того, чтобы разбить это на этот класс запросов?

final class RecentAccountsForUser implements RecentAccountsForUserContract
{
    public function handle(Builder $query, int $days = 7): Builder
    {
        return $query->select(
            'accounts.*',
            DB::raw('COUNT(transactions.id) as total_transactions')
        )->groupBy('transactions.account_id');
    }
}

Тогда в нашей реализации это выглядело бы следующим образом:

public function __invoke(Request $request): JsonResponse
{
    $accounts = $this->query->handle(
        query: DB::table(
            'transactions'
        )->join(
            'accounts',
            'transactions.account_id', '=', 'accounts.id'
        )->where('accounts.user_id', auth()->id()),
    )->orderBy('transactions.created_at', 'desc')->get();
 
    // handle the return.
}

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

В целом, то, что мы сделали, - это создали нечто похожее на scopes, но менее привязанное к самому Eloquent Builder.

Именно так мне нравится управлять своими красноречивыми запросами в моих приложениях, поскольку это позволяет мне иметь повторяющиеся части, которые можно тестировать изолированно от различных входящих опций. Мне нравится думать об этом как об эффективном подходе к написанию запросов, но он не для всех - моя недавняя статья с Мэттом Стауффером доказывает это! Все, что я только что сделал, может быть достигнуто с помощью вспомогательных методов для моделей или даже областей запросов - но мне нравится, чтобы мои модели были простыми, а мои скоупы - легкими и специфичными. Добавление слишком большого количества логики в один скоуп мне кажется неправильным. Такое чувство, что ему здесь не место. Конечно, я могу ошибаться, и я всегда рад признать, что мой путь - не единственный способ приблизиться к этому.

Оригинал статьи -https://laravel-news.com/effective-eloquent