Lazy Loading является механизмом EF Core, с помощью которого неявно загружаются связанные сущности при первом обращении к их навигационным свойствам. Для того, чтобы EF Core мог использовать такой тип загрузки связанных данных, все навигационные свойства модели должны быть
virtual.public class Library
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Book> Books { get; set; }
}
А также при конфигурировании контекста необходимо вызвать
UseLazyLoadingProxies(), включив поддержку прокси.protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies() // Включение прокси для Lazy Loading
.UseNpgsql("Host=localhost;Database=mydb;");
}
Теперь необязательно при вытягивании
Library (context.Library.FirstOrDefault()) необязательно явно делать получение Books через Include или Load, будет достаточно просто обратиться к данному свойству и получим необходимые данные. Но как это работает, минусы будут, почему это антипаттерн, когда так все упрощается?!
Lazy Loading реализуется через динамические прокси-объекты, т.е. оно работает на основе наследования и переопределения виртуальных свойств. Когда мы запрашиваем сущность (
context.Library.FirstOrDefault()), EF не возвращает саму сущность, а создаёт наследника от неё в рантайме!Таким образом EF создаст динамический подкласс
LibraryProxy, который наследуется от Library и переопределяет свойство Books таким образом, что при первом запросе автоматически выполняется sql-запрос к бд для загрузки данных в коллекцию (SELECT * FROM Books WHERE LibraryId = @Id), а при последующих данные будут доставаться уже из коллекции из памяти.И к чему это может привести?! Как минимум, это уже неэффективно из-за того, что для генерации в рантайме таких прокси-классов EF использует
Castle.DynamicProxy, использующий в свою очередь рефлексию, а использование виртуальных таблиц - это дополнительные расходы по памяти и по времени запроса. А как максимум, мы получаем кучу неявных обращений к БД (которые в ряде случаев нужно упаковать в один запрос), и потенциальную проблему N + 1 🤡.⚠️ Проблема "N+1 запросов":
Если мы загружаем 10 Library и потом в цикле обращаемся к Books, это приведёт к 10 дополнительным запросам к БД. (А если таких сущностей 1000?))
Таким образом, данный подход ведет только к потенциальным проблемам с производительностью системы, а плюсы в удобстве, что не нужно явно прописывать загружаемые сущности в запросе, весьма сомнительны. Не знаю ни одного кейса, где Lazy был бы действительно полезен.
🤡 Допустим, вам достался проект от предыдущей некомпетентной команды, которая взяла и втащила Lazy на глобальном уровне, и в куче мест получаем проблемы с производительностью. Что делать, как избавляться от данного говна?)
Как вариант, стоит договориться с командой в новом коде загружать данные только явным образом и для начала убрать Lazy на глобальном уровне и прописать его для всех моделей сущностей отдельно через Delegate-based Lazy Loading, что решит проблему с излишней кодогенерацией и позволит постепенно выпиливать Lazy для отдельных сущностей.
public class Library
{
private readonly ILazyLoader _lazyLoader;
private ICollection<Book> _books;
public Library() { }
public Blog(ILazyLoader lazyLoader)
{
_lazyLoader = lazyLoader;
}
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Book> Books
{
get => _lazyLoader.Load(this, ref _books);
set => _books = value;
}
}
Однако такой подход не решает проблему N+1 и нужно все равно искать данные места и исправлять их, переводя загрузку сущностей на явную, а не через 100500 отдельных неявных запросов.