Laravel 5 Отношения и жадная загрузка в Eloquent

Laravel 5

laravel

Отношения и жадная загрузка в Eloquent

На основе статьи  https://bosnadev.com (англ)

Предисловие

Прежде чем перейти к основной теме статьи, я хотел бы привести случай из практики. Один из моих клиентов пожаловался, что некоторые страницы его сайта загружаются слишком медленно. Я был в шоке, когда узнал причину: на каждой проблемной странице было 16500+ запросов к базе данных. Код состоял из 3 циклов foreach с запросами данных, связанных друг с другом в разных таблицах. Все работало более менее нормально до тех пор, пока количество записей не стало около 5500.  Вот пример кода:

Очень часто программисты пишут такие неоптимальные запросы и при этом использование ORM лишь усложняет проблему, известную как проблема N+1. Думаю, предыдущий разработчик моего клиента не знал о том, что решение заключается в жадной загрузке (eager loading).

Что такое "жадная загрузка" (eager loading)?

Есть три подхода: жадная загрузка (eager loading), ленивая загрузка (lazy loading) и сверхжадная загрузка (over-eager loading).

Жадная загрузка - загружаем все, что может понадобиться, ленивая загрузка - загружаем только то, что требуется, сверхжадная загрузка - загружаем то, что требуется и то, что скорее всего понадобится. Поясним на конкретном примере: страница с выпадающим меню, в каждом пункте меню есть изображение, которое показывается при раскрытии. Отображение страницы может происходит в трех вариантах:

  1. Загрузка всех изображений до показа страницы (жадная загрузка)
  2. Загрузка только тех изображений, который будут сразу показаны, остальные будут загружены при необходимости (ленивая загрузка)
  3. Загрузка только тех изображений, который будут сразу показаны, затем будут подгружаться изображения, которые могут понадобиться (сверхжадная загрузка)

Другими словами, жадная загрузка делает все, что просят, в противоположность ленивой загрузке, которая делает только то, что необходимо. Давайте разбираться на примере в Laravel. Рассмотрим схему:

Это EER-модель (Enhanced entity–relationship model) с тремя взаимосвязанными сущностями. У каждого владельца (member) может быть много магазинов (store), но каждый магазин  может принадлежать только одному владельцу. Каждый магазин может иметь несколько товаров (product), но каждый товар может принадлежать только одному магазину.

Cоздадим Eloquent модели для этих сущностей.

Владелец

Магазин

Товар

Представьте, что пишите приложение, которая позволяет пользователям создавать свои собственные магазины. Конечно, они могут добавлять и товары. И пусть от нас требуется создать страницу со списком всех магазинов, для каждого из которых показан владелец и лучшие товары. Что-то вроде такого:

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

И вид (view):

Результат (в базе 5 владельцев, 3 магазина и 4 товара):

Сначала мы запрашиваем все магазины и это "+1" из "проблема N+1". А "N" - это количество магазинов. Далее мы много раз обращаемся select * from к таблицам products и members. Так как 3 магазина, то будет 3 запроса к members и 3 дополнительных запроса к products. Всего 3+3+1 запрос.

Если у нас 5000 или 10000 магазинов, то при каждом посещении страницы будет выполняться 10 000 - 20 000 запросов. И если посещений 10 000 в сутки? Понятно, что такой код неприемлем. И неважно какую базу вы используете, какой мощности у вас сервер. И даже кеширование запросов (например, Redis) будет лишь временным решением.

Применять жадную загрузку в Laravel довольно просто: указываем необходимые отношения в методе with(). Пример: $stores = Store::with('member','products')->get().

И получаем всего три запроса даже при 10000 магазинах. Чтобы еще улучшить производительность, мы должны добавить индекс к id-полям в таблицах members и products, так как запрос с in('1','2',...) может занять длительное время без индексации.

Расширение класс репозитория

Покажем, как можно работать с отношениями в репозиториях. Вот что должно получиться:

Как видите, у нас есть метод with(), который очень похож на метод with() стандартного Query Builder-а в Laravel.

Давайте привяжем отношения к модели:

И обновим метод all() в репозитории для жадной загрузки:

Как я уже говорил, вы можете добавить несколько отношений в with(). Вот пример для StoresController:

Вид:

В результате, как и ожидалось, всего 3 запроса:

Вывод

С помощью жадной загрузки вы можете повысить производительность вашего приложения. Хотя в некоторых случаях такого подхода недостаточно и приходится использовать кеширование запросов и другие приемы.

все материалы по Laravel 5