Сортировка запроса Eloquent по возвращаемому целому числу пользовательской функции расчета прогресса в Laravel

Вопрос или проблема

В моем приложении Laravel есть модель Group, которая включает в себя пользовательский метод progress() для вычисления прогресса каждой группы. Этот метод вычисляет прогресс на основе количества сессий и посещаемости.

Вот метод progress():

public function progress(): int
{
    $groupMembersCount = $this->groupMembers->count();
    $sessionsNumber = $this->sessionsNumber;
    $progress = 0;

    // Перебирать сессии
    for ($i = 1; $i <= $sessionsNumber; $i++) {
        $session = $this->sessions->where('sessionNumber', $i)->first();

        if ($session) {
            $sessionAttendancesCount = $session->attendances->count();

            // Проверить, соответствует ли посещаемость количеству членов группы
            if ($sessionAttendancesCount == $groupMembersCount) {
                $progress++;
            }
        }
    }

    return $progress;
}

Я хочу отсортировать группы по этому методу progress() при получении групп в запросе Eloquent. Вот запрос, в котором мне нужно интегрировать эту сортировку:

$groups = Group::query()
    ->search($this->search)
    ->teacherFilter($this->teacherID)
    ->languageFilter($this->languageID)
    ->levelFilter($this->levelID)
    ->ageRangeFilter($this->ageRange)
    ->dayFilter($this->day)
    ->timeFilter($this->time)
    ->where('status', 'LIKE', "%{$this->status}%")
    ->orderBy($this->sortBy, $this->sortDirection)
    ->paginate($this->perPage);

Как я могу изменить этот запрос, чтобы отсортировать группы на основе результата метода progress()?

Я пытался интегрировать метод progress() напрямую в запрос, но так как метод progress() выполняет вычисления на основе загруженных отношений (члены группы, сессии и посещаемость), это не так просто использовать его в запросе Eloquent или с orderBy.

Ответ или решение

Чтобы отсортировать результаты выборки групп в Laravel на основании значения, возвращаемого методом progress(), необходимо учитывать, что этот метод использует отношения, которые не могут быть непосредственно вызваны в SQL-запросах. Однако есть несколько способов решения этой задачи. В данной статье мы рассмотрим несколько подходов.

Подход 1: Использование объединения и подзапросов

Первым подходом является использование подзапросов для вычисления прогресса каждой группы. В этом случае мы можем создать запрос внутри метода selectRaw, который будет аналогичен методу progress().

$groups = Group::query()
    ->search($this->search)
    ->teacherFilter($this->teacherID)
    ->languageFilter($this->languageID)
    ->levelFilter($this->levelID)
    ->ageRangeFilter($this->ageRange)
    ->dayFilter($this->day)
    ->timeFilter($this->time)
    ->where('status', 'LIKE', "%{$this->status}%")
    ->select('groups.*')
    ->selectRaw('(SELECT COUNT(*) FROM sessions s 
                JOIN attendances a ON s.id = a.session_id 
                WHERE s.group_id = groups.id 
                GROUP BY s.id 
                HAVING COUNT(a.id) = (SELECT COUNT(*) FROM group_members gm WHERE gm.group_id = groups.id)) AS progress')
    ->orderBy('progress', 'desc')
    ->paginate($this->perPage);

Подход 2: Использование коллекций

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

$groups = Group::query()
    ->search($this->search)
    ->teacherFilter($this->teacherID)
    ->languageFilter($this->languageID)
    ->levelFilter($this->levelID)
    ->ageRangeFilter($this->ageRange)
    ->dayFilter($this->day)
    ->timeFilter($this->time)
    ->where('status', 'LIKE', "%{$this->status}%")
    ->get();

// Сортируем группы по прогрессу
$groups = $groups->sortByDesc(function ($group) {
    return $group->progress();
});

// Пагинатор — необходимо вручную выполнить для коллекции
$perPage = $this->perPage;
$page = LengthAwarePaginator::resolveCurrentPage();
$groups = new LengthAwarePaginator(
    $groups->forPage($page, $perPage),
    $groups->count(),
    $perPage,
    $page,
    ['path' => LengthAwarePaginator::resolveCurrentPath()]
);

Подход 3: Оптимизация через индексы и агрегацию

Для более эффективных запросов, особенно при работе с большими объемами данных, подумайте о создании индексов для ключей, используемых в подсчетах. Например, если у вас есть индексы на полях group_id в таблице sessions и session_id в таблице attendances, это может значительно ускорить операции выборки.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...