Одной из распространенных проблем программистов при создании приложения с помощью ORM является N + 1 запрос в их приложении.
Проблема N+1 – это неэффективный способ запроса базы данных, когда наше приложение делает множество дозапросов к связным моделям. Проблема чаще возникает когда мы получаем список сущностей без реализации метода ленивой загрузки. К счастью, Laravel с его Eloquent имеет инструменты ленивой загрузки, которыми мы можем указать, какие связные сущности мы можем сразу включать в запрос.
Простой пример использования ленивой загрузки. У нас есть модели пользователя и статей. У пользователя есть связь 1 к М со статьями.
Сделаем простую страницу, на которой будем выводить все статьи пользователей
В шаблоне мы выведем всех пользователей и по одной самой первой статье каждого пользователя
И когда наша страница будет открыта, мы увидим следующий список запросов
Как мы видим, было выполнено 11 запросов, из которых 1 для получения списка пользователей и 10 для получения первой статьи по каждому пользователю. Это условие называется N+1 проблемой запроса.
Решение задачи запроса N+1 с жадной загрузкой
Если не решать эту проблему, мы можем столкнуться с гораздо бОльшим кол-вом запросов и даже экспоненциальному их росту.
Жадноя загрузка – это процесс, при котором запрос для одного типа сущности также загружает связанные сущности как часть запроса. В Laravel мы можем загружать соответствующие модельные данные с помощью with(..) метода. В нашем примере мы должны изменить наш код со следующими изменениями:
И, наконец, мы можем уменьшить кол-во наших запросов до всего лишь 2 запросов:
Мы также можем создать связь hasOne с соответствующим запросом для извлечения первой статьи пользователя:
Тогда мы можем загрузить его, когда мы загружаем наших пользователей:
Вот вам и результат:
Отлично. Мы сократили кол-во запросов с 11 до 2. Мы решили проблему N+1. Но есть нюанс =) Обратите внимание на кол-во загруженных моделей. С 20 их кол-во внезапно увеличилось до 10010.
Динамические связи и жадная загрузка
Основные цели при создании веб-приложения:
- минимум запросов к базе данных
- минимум использованной памяти
В примерах выше мы смогли сохранить либо малое кол-во запросов, либо малое использование памяти.
Для решения возникшей проблемы мы можем воспользоваться динамической жадной загрузкой через подзапросы.
Ниже приведен пример извлечения данных пользователей с соответствующим идентификатором первой статьи в рамках подзапроса:
После этого мы можем использовать first_article_id с отношением belongs_to для извлечения первых статей пользователя. Чтобы сделать наш код более понятным, мы также можем использовать Eloquent scopes для инкапсуляции нашего запроса и жадной загрузки для первой статьи. Таким образом, мы должны добавить код ниже в нашу модель пользователя:
И наконец, давайте изменим наше действие и посмотрим, что получилось. Мы должны использовать scope в нашем действии, чтобы жадно загрузить первую статью. И тогда мы можем непосредственно обращаться к атрибуту first_article в представлении.
Вот вам и результат:
Теперь на нашей странице всего 2 запроса и 20 загруженных моделей. И мы достигли наших целей, оптимизируя запросы к базе данных и использование памяти до минимума.
Остались вопросы?