Как оптимизировать этот метод и избежать цикла foreach - PullRequest
1 голос
/ 03 марта 2020

В моем приложении Laravel есть следующие модели: курс, мероприятие, студент, регистрация, посещаемость.

  • курс имеет много событий.
  • при зачислении студента В Курсе создается Зачисление, поэтому в курсе имеется Много Записей.
  • У студента есть множество записей об участии.
  • Запись об участии принадлежит Событию и принадлежит Студенту.

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

В качестве временного решения я использую вложенный foreach l oop в моей модели курса:

public function getEventsWithExpectedAttendanceAttribute()
{
    return $this->events()->where(function($query) {
        $query->where('exempt_attendance', '!=', true);
        $query->where('exempt_attendance', '!=', 1);
        $query->orWhereNull('exempt_attendance');
    })->where('start', '<', Carbon::now(env('COURSES_TIMEZONE'))->toDateTimeString())->get();
}

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        // loop through every student
        foreach ($this->enrollments as $enrollment)
        {
            // if the student has no attendance record for this event
            if (Attendance::where('student_id', $enrollment->student_id)->where('event_id', $event->id)->count() == 0)
            {
    // count one and break loop
            $eventsWithMissingAttendanceCount++;
            break;
            }
        }
    }

    return $eventsWithMissingAttendanceCount;
}

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

ОБНОВЛЕНИЕ: вот схема БД

CREATE TABLE `courses` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`));

CREATE TABLE `events` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `course_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `enrollments` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `student_id` int(10) unsigned NOT NULL,
  `course_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `students` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`));

CREATE TABLE `attendances` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `student_id` int(10) unsigned NOT NULL,
  `event_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`));

Ответы [ 2 ]

0 голосов
/ 04 марта 2020

enrollments (например) таблица сопоставления «многие: многие». Определение схемы у вас очень неэффективно. Это дает несколько советов по улучшению производительности в этой области: http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table

0 голосов
/ 03 марта 2020

Ваш метод имеет квадратичную c n ^ 2 производительность, так как количество запросов умножается на количество событий и посещаемости.

Первым шагом к сокращению количества запросов может быть извлечение этого запроса из самого внутреннего l oop

Attendance::where('student_id', $enrollment->student_id)->where('event_id', $event->id)->count() == 0

и получать посещаемость заранее

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    $eventsIDs = $this->events_with_expected_attendance->pluck('id');
    $studentIDs = $this->enrollments->pluck('student_id');

    // Collection of attendances
    $attendances = Attendance::whereIn('student_id', $studentIDs)
        ->whereIn('event_id', $eventsIDs)
        ->get();


    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        // loop through every student
        foreach ($this->enrollments as $enrollment)
        {
            $hasNotAttended = $attendances->where('student_id', $enrollment->student_id)
                ->where('event_id', $event->id)
                ->isEmpty();

            if ($hasNotAttended) {
                $eventsWithMissingAttendanceCount++;
                break;
            }
        }
    }

    return $eventsWithMissingAttendanceCount;
}

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

Например, вы можете извлечь $hasNotAttended для метода модели регистрации.

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    $eventsIDs = $this->events_with_expected_attendance->pluck('id');
    $studentIDs = $this->enrollments->pluck('student_id');

    // Collection of attendances
    $attendances = Attendance::whereIn('student_id', $studentIDs)
        ->whereIn('event_id', $eventsIDs)
        ->get();


    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        // loop through every student
        foreach ($this->enrollments as $enrollment)
        {
            // pass preloaded attendances.
            // alternatively you could preload this relationship then no need to pass $attendances
            if ($enrollment->hasNotAttended($event->id, $attendances)) {
                $eventsWithMissingAttendanceCount++;
                break;
            }
        }
    }

    return $eventsWithMissingAttendanceCount;
}

Затем вы можете расширить $this->enrollments collection https://laravel.com/docs/6.x/collections#extending -collections , извлекая следующее.

    if ($enrollment->hasNotAttended($event->id, $attendances)) {
        $eventsWithMissingAttendanceCount++;
        break;
    }

Таким образом, это будет упрощено до

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    $eventsIDs = $this->events_with_expected_attendance->pluck('id');
    $studentIDs = $this->enrollments->pluck('student_id');

    // Collection of attendances
    $attendances = Attendance::whereIn('student_id', $studentIDs)
        ->whereIn('event_id', $eventsIDs)
        ->get();


    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        if ($this->enrollments->hasNotAttended($event->id, $attendances)) {
            $eventsWithMissingAttendanceCount++;
        }
    }

    return $eventsWithMissingAttendanceCount;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...