Eloquent 최적화: 일대다 관계의 레코드 하나만 표시

8701 단어
경우에 따라 일대다 관계에서 하나의 레코드에 대한 데이터만 표시해야 합니다. 예를 들어:
  • 각 사용자가 게시한 마지막 비디오의 제목 표시
  • 각 카테고리에서 가장 많이 판매된 제품 표시 중
  • 모든 블로그 게시물의 마지막 댓글 표시
  • 기타

  • 이 기사에서는 이 작업을 수행할 수 있는 다양한 방법을 살펴보고 시간과 메모리 측면에서 가장 효율적인 방법을 알아내고자 합니다.

    순진한 접근



    이 기사에서는 사용자가 동영상을 게시할 수 있는 플랫폼의 예를 사용하고 각 사용자의 마지막 동영상 제목을 표시하려고 합니다.

    이를 수행하는 한 가지 방법은 컨트롤러에서 사용자를 검색한 다음 블레이드 템플릿에서 비디오를 created_at 날짜별로 정렬하고 첫 번째를 가져온 다음 제목을 표시하는 것입니다.

        return view('users', ['users' => User::get()]);
    



        @foreach($user as $users)
            <!-- Table html -->
            <td>{{ $user-> }}</td>
            <td>{{ $user->videos->sortByDesc('created_at')->first()->title }}</td>
        @endforeach
    


    이 데이터베이스에서 쿼리 성능을 파악하기 위해 30명의 사용자에게 각각 100개의 비디오를 시드했습니다.

    Laravel 디버그바를 사용하여 데이터베이스에 32개의 쿼리를 만들고 330개의 모델을 메모리에 로드하는 것을 볼 수 있습니다!

    이것은 우리가 하고 있는 일에 매우 비효율적입니다. 이 쿼리를 수정해 보겠습니다.

    n + 1 문제 해결



    n + 1 문제에 익숙하지 않은 경우 제가 말하는 것은 사용자당 하나의 쿼리를 생성한다는 것입니다. 101개의 쿼리를 작성합니다.

    이것이 현재 애플리케이션에서 직면하고 있는 문제입니다. Laravel이 이 문제를 해결하기 위해 제공하는 한 가지 방법은 즉시 로딩을 사용하는 것입니다. 그리고 우리는 설득력 있는 쿼리에 with('videos') 호출을 추가하여 이를 수행할 수 있습니다.

        return view('users', ['users' => User::with('videos')->get()]);
    


    이 간단한 변경으로 n + 1 문제가 해결되었습니다. 지금 Laravel 디버그 표시줄 출력을 보면 쿼리가 2로 줄었음을 알 수 있습니다!

    이것은 큰 개선이지만 여전히 330개의 모델을 메모리에 로드하고 있습니다. 즉, 데이터베이스에서 모든 사용자의 모든 비디오를 검색하고 있습니다. 문제를 해결해 보겠습니다.

    마지막 비디오만 가져오기



    다행스럽게도 Laravel은 최신 비디오에 대한 관계를 생성하는 간단한 방법을 제공하며 관계는 사용자 모델에서 다음과 같이 표시됩니다.

        public function latestVideo()
        {
            return $this->hasOne(Video::class)->latestOfMany();
        }
    


    이렇게 하면 $user->latestVideo 를 사용하여 사용자의 마지막 비디오만 검색할 수 있으며 관계이기 때문에 열심히 로드할 수도 있습니다. 컨트롤러에서 새로운 관계를 활용해 봅시다.

        return view('welcome', ['users' => User::with('latestVideo')->get()]);
    


    그리고 블레이드 템플릿에서:

        @foreach($user as $users)
            <!-- Table html -->
            <td>{{ $user-> }}</td>
            <td>{{ $user->latestVideo->title }}</td>
        @endforeach
    


    이제 Laravel debugbar를 확인하면 Eager Loading을 사용하고 있기 때문에 2개의 쿼리만 만들고 있고 60개의 모델을 메모리에 로드하고 있으며 30명의 사용자와 30개의 비디오를 로드하고 있습니다.

    하지만 여전히 조금 더 개선할 수 있다고 생각합니다. 비디오 모델을 로드할 필요가 없고 대신 제목만 가져오면 어떨까요?

    마지막 비디오 제목만 가져오기



    최신 비디오의 제목만 가져오려면 addSelect 메서드를 사용할 수 있습니다. 이 방법을 사용하여 쿼리에 열로 추가될 하위 선택을 추가할 수 있습니다. 내가 생각해 낸 것은 다음과 같습니다.

        $users = User::addSelect(['latest_video_title' => Video::query()
            ->select('title')
            ->whereColumn('user_id', '=', 'users.id')
            ->orderByDesc('created_at')
            ->limit(1) 
        ])->get();
    
        return view('users', ['users' => $users]);
    


    이 쿼리에서 제목을 선택하고 비디오가 사용자에게 속하는지 확인하는 위치를 추가하여 created_at 열의 결과를 내림차순으로 정렬한 다음 결과를 1로 제한하여 첫 번째 항목을 가져옵니다.

    이것은 이전 방법만큼 읽기 쉽지 않으므로 컨트롤러에서 이 작업을 수행하는 대신 사용자 모델에서 범위를 생성해 보겠습니다.

        public function scopeWithLastestVideoTitle($query)
        {
            $query->addSelect(['lastest_video_title' => Video::query()
                ->select('title')
                ->whereColumn('user_id', '=', 'users.id')
                ->orderByDesc('created_at')->limit(1) 
            ]);
        }
    


    컨트롤러에서 범위를 호출할 수 있습니다.

        return view('welcome', ['users' => User::withLastestVideoTitle()->get()]);
    


    그런 다음 이 새로 생성된 열을 사용하도록 블레이드 템플릿을 변경해야 합니다.

        @foreach($user as $users)
            <!-- Table html -->
            <td>{{ $user-> }}</td>
            <td>{{ $user->lastest_video_title }}</td>
        @endforeach
    


    마지막으로 Laravel 디버그바를 사용하여 페이지를 32개의 쿼리를 만들고 330개의 모델을 로드하는 것에서 2개의 쿼리를 만들고 30개의 모델만 메모리에 로드하는 것으로 페이지를 얻었음을 알 수 있습니다. 꽤 깔끔한!

    이 기사가 마음에 든다면 거기에 팁과 기타 Laravel 콘텐츠를 게시합니다.

    좋은 웹페이지 즐겨찾기