Ленивая и нетерпеливая загрузка в Laravel

 

Это работает на всех взаимосвязях, но рассмотрим пример на простой взаимосвязи hasMany. Итак, очень простой пример: есть Категория (модель Cat) и есть Товар (модель Product). В каждой категории может быть несколько товаров Cat hasMany Products.

В модели Cat это, соответственно, выглядит так:

    //файл /app/Models/Cat.php
    
    public function products()
    {
        return $this->hasMany(Product::class);
    }
  

 

Допустим, товаров каждой категории у нас мало и категорий мало и нам нужно вывести:

  1. страничку index со списком всех категорий и принадлежащих им товаров
  2. страничку show/{id} с конкретной категорией по выбранному {id} и принадлежащим ей товаром

В контроллере CatsController у нас должно быть две функции index и show, которые выводят, соответственно 1) и 2) пункт.

    //файл /app/Http/Controllers/CatsController.php
    
    //функция index, отвечающая за функционал 1)-го пункт
    public function index()
    {
        //получаем список всех категорий из бд
        $cats = Cat::get();

       //передаем переменную с массивом полученных категорий в вьюер cats
        return view('cats.index',compact('cats'));
    }

    //функция show, отвечающая за функционал 2)-го пункта
    public function show($id)
    {
        //находим нужную категорию по id
        $cat = Cat::findOrFail($id);

       //передаем переменную с массивом найденной категорий в вьюер cats
        return view('cats.show',compact('cat'));
    }
  

 

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

Осталось сделать вьюер и прописать в нем вывод данных из полученных массивов категорий. Для index это будет выглядеть так

    //файл /resources/views/cats/index.blade.php
    
    //для каждой категории из массива категорий
   @foreach($cats as $cat)
       //выводим название каждой категории
       {{ $cat->name }} <br />
       //для каждого товара, привязанного к категории (функция products из модели Cat)
       @foreach($cat->products as $product)
           //выводим название товара
           {{ $product->name }} <br />
       //конец цикла foreach товаров
       @endforeach
   //конец цикла foreach категорий
   @endforeach

  

 

Для show это будет выглядеть также, но без лишнего foreach

    //файл /resources/views/cats/show.blade.php
    
       //выводим название категории
       {{ $cat->name }} <br />
       //для каждого товара, привязанного к категории (функция products из модели Cat)
       @foreach($cat->products as $product)
           //выводим название товара
           {{ $product->name }} <br />
       //конец цикла foreach товаров
       @endforeach

  

 

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

Сейчас про запросы к БД. Если посмотреть дебаг запросов к БД данного кода, то увидим, что для каждой категории создается отдельный запрос к БД, который выводит список товаров категории. Выглядит это так:

    //вывод запросов к бд из дебага
    
   select * from 'products' where 'cat_id' = ...;

  

 

Это называется "ленивая загрузка" (lazy load). Привязанные к текущей модели Cat элементы из модели Product не выводятся, пока не запросят их вывод через вьюер методом $cat->products. Т.е. без данного метода запрос к базе данных не будет осуществляться. Но при наличии данного метода по каждой из категорий будет создаваться отдельный запрос к БД для вывода списка товаров данной конкретной категории.

Т.е. lazy load удобно использовать там, где только одна категория (в функции show), но не очень правильно с точки зрения нагрузки на БД использовать там где несколько категорий (в функции index).

Чтобы избежать этого существует "нетерпеливая загрузка". Данный подход предполагает использование метода with()

Вернемся к функции index, отвечающей за функционал 1)-го пункта нашего контроллера.

    //файл /app/Http/Controllers/CatsController.php
    
    //функция index, отвечающая за функционал 1)-го пункт
    public function index()
    {
        //получаем список всех категорий из бд с помощью "нетерпеливой загрузки"
        $cats = Cat::with('products'):
            ->get();

       //передаем переменную с массивом полученных категорий в вьюер cats
        return view('cats.index',compact('cats'));
    }
  

 

Больше ничего менять не нужно. Таким образом мы меняем "ленивую" загрузку на "нетерпеливую" загрузку и убираем несколько запросов к БД по каждой из категорий, оставляя один запрос вида:

    //вывод запросов к бд из дебага
    
   select * from 'products' where 'cat_id' in (1, 4, 8, ...);

  

 

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

      $cats = Cat::with(['products','comments','posts'])
          ->get();
  

 

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

      $cats = Cat::with(['products',
          'posts' => function ($query) {
              $query->where('user_id', '=', '1');
          },
          'comments' => function ($query) {
              $query->where('user_id', '=', '1');
          }
      ])
      ->get();
  

 

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

Допустим, у товара есть комментарии и обзоры по принципу Product hasMany Comments и Product hasMany Reviews. Каждый Comment и Review привязан к пользователю User через user_id. Нам нужно вывести в функции index список товаров с привязанными активными комментариями и обзорами текущего авторизованного пользователя к товару если пользователь авторизован. Т.е. пользователь, если авторизовался, то должен увидеть только свои комментарии и обзоры к товару. Товар также имеет привязанные свойства и аналоги (Product hasMany Props и Product hasMany Analogs) но они не привязаны к пользователям.

Сделать это можно так

   //файл /app/Http/Controllers/ProductsController.php

    public function index()
    {
        //задаем массив привязанных свойств и аналогов, 
        //не нуждающихся в дополнительных условиях
        $with = ['props','analogs'];

        //если пользователь авторизован
        if (Auth::user()) {

            //задаем массив комментариев с дополнительным условием
            // отбора по id текущего авторизованного пользователя
            $with['comments'] = function ($query) {
                $query->where('user_id', '=', Auth::user()->id);
            };

            //задаем массив обзоров с дополнительным условием
            // отбора по id текущего авторизованного пользователя
            $with['reviews'] = function ($query) {
                $query->where('user_id', '=', Auth::user()->id);
            };

        }

         // отбираем список товаров по нужным параметрам из БД
         $products = Product::with($with)->get();

        // передаем список полученных товаров во вьюер
        return view('products.index',compact('products'));
    }

  

 

Кейсы