Какие преимущества и недостатки ленивой загрузки (lazy loading) в Entity Framework
Что проверяют
Знаете ли вы, как ленивая загрузка устроена в EF Core и почему её часто не включают. Следом почти наверняка спросят про проблему N+1 и способы её лечить.
Как отвечать
Уточните версию. В EF Core ленивая загрузка выключена по умолчанию: нужен пакет Microsoft.EntityFrameworkCore.Proxies и вызов UseLazyLoadingProxies(), а навигационные свойства должны быть virtual. Вариант без прокси: внедрить в сущность ILazyLoader. В классическом EF6 она была включена по умолчанию, отсюда много старых советов.
Три способа загрузить связи:
- eager:
IncludeиThenInclude, связи приходят сразу, одним запросом или несколькими сAsSplitQuery(); - explicit:
context.Entry(order).Collection(o => o.Items).LoadAsync(), догружаете вручную, когда решили; - lazy: данные подтягиваются при первом обращении к навигационному свойству.
Плюсы ленивой загрузки: не нужно заранее знать, какие связи понадобятся, и то, к чему не обратились, не читается. Удобно в прототипах и там, где связь нужна редко.
Минусы весомее:
- N+1: цикл по 100 заказам с обращением к
order.Customerдаёт 101 запрос; - скрытые запросы: обращение к свойству выглядит как чтение поля, а идёт в базу, на ревью этого не видно;
- синхронность: асинхронной ленивой загрузки нет, поток блокируется на запросе;
- жизнь контекста: после
Disposeконтекста обращение к незагруженной связи бросает исключение; - сериализация: сериализатор обходит навигационные свойства и может вытянуть полбазы или упасть на циклической ссылке.
Где спотыкаются
Говорят, что ленивая загрузка включена по умолчанию. Это верно для EF6, но не для EF Core.
Не знают, как заметить N+1. Помогает логирование SQL через LogTo в разработке, профилировщик базы и проверка числа запросов в тестах.
Лечат всё через Include на все связи подряд. Для списков лучше проекция через Select в DTO: база отдаёт только нужные колонки, трекинг изменений не включается, а связи подтягиваются в том же запросе.