Какие способы загрузки связанных данных (dependent entities) существуют в Entity Framework?

Ответ

Entity Framework Core предоставляет три основных стратегии загрузки связанных данных (навигационных свойств). Выбор зависит от сценария использования и требований к производительности.

1. Жадная загрузка (Eager Loading)

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

var order = context.Orders
    .Include(o => o.Customer)           // Загружаем связанную сущность Customer
    .Include(o => o.Items)              // Загружаем коллекцию Items
        .ThenInclude(i => i.Product)    // Загружаем Product для каждого Item
    .FirstOrDefault(o => o.Id == orderId);

Плюсы: Один запрос к БД, предсказуемое время выполнения. Минусы: Может привести к избыточному объему данных (over-fetching) и сложным SQL-запросам с множеством JOIN.

2. Явная загрузка (Explicit Loading)

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

var order = context.Orders.Find(orderId);
// Загружаем коллекцию
context.Entry(order)
    .Collection(o => o.Items)
    .Load();
// Загружаем ссылочное свойство
context.Entry(order)
    .Reference(o => o.Customer)
    .Load();

Плюсы: Полный контроль над моментом загрузки. Минусы: Множественные запросы к БД (проблема N+1, если делать в цикле).

3. Ленивая загрузка (Lazy Loading)

Связанные данные загружаются автоматически при первом обращении к навигационному свойству. Требует установки пакета Microsoft.EntityFrameworkCore.Proxies и настройки.

// 1. Установка в DbContext:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseLazyLoadingProxies();

// 2. Навигационные свойства должны быть `virtual`:
public class Order
{
    public virtual Customer Customer { get; set; }
    public virtual ICollection<OrderItem> Items { get; set; }
}
// Использование:
var order = context.Orders.Find(orderId);
var customerName = order.Customer.Name; // Здесь выполнится отдельный запрос к БД

Плюсы: Удобство разработки, код не загроможден Include. Минусы: Высокий риск непреднамеренных запросов N+1, что сильно снижает производительность. Затрудняет понимание реального количества SQL-запросов.

Рекомендация: Для большинства сценариев жадная загрузка с проекцией (Select) является оптимальным выбором, так как позволяет контролировать форму и объем данных одним запросом.

var orderData = context.Orders
    .Where(o => o.Id == orderId)
    .Select(o => new // Проекция в DTO
    {
        o.Id,
        CustomerName = o.Customer.Name,
        Items = o.Items.Select(i => new { i.Id, i.Product.Name })
    }).FirstOrDefault();

Ответ 18+ 🔞

А, Entity Framework, блядь! Ну, слушай, тут про загрузку связанных данных расскажу, а то народ постоянно в это влипает, как мухи в говно.

Вот смотри, есть три основных способа, как EF Core эти твои навигационные свойства подтягивает. И каждый из них — это отдельная история с подводными ебенями.

Первый — жадная загрузка (Eager Loading). Это когда ты сразу, одним махом, всё загружаешь, как алкаш в пятницу всю зарплату в магазине оставляет. Используешь Include и ThenInclude.

var order = context.Orders
    .Include(o => o.Customer)           // Заказчика цепляем
    .Include(o => o.Items)              // Позиции в заказе
        .ThenInclude(i => i.Product)    // А для каждой позиции — ещё и товар
    .FirstOrDefault(o => o.Id == orderId);

Плюсы? Один запрос в базу, и всё приехало. Предсказуемо, быстро, если правильно индексы стоят. Минусы? А минусы в том, что запрос может превратиться в такую простыню с кучей JOIN, что база захлебнётся, а ты получишь дохуя лишних данных, которые тебе нахуй не сдались. Over-fetching называется, умным словом.

Второй — явная загрузка (Explicit Loading). Это уже для контроль-фриков. Сначала грузишь основную сущность, а потом, когда захочешь, отдельным запросом подтягиваешь связанные данные. Полный контроль, как в аптеке.

var order = context.Orders.Find(orderId); // Нашли заказ
// А теперь, по щучьему веленью, грузим позиции
context.Entry(order)
    .Collection(o => o.Items)
    .Load();
// И заказчика не забудь
context.Entry(order)
    .Reference(o => o.Customer)
    .Load();

Плюсы? Сам решаешь, когда и что грузить. Минусы? Если это в цикле делать, то получишь классическую проблему N+1, где на каждую сущность будет отдельный запрос. База тебя возненавидит, а приложение будет тормозить, как танк в болоте.

Третий — ленивая загрузка (Lazy Loading). О, это любимая фишка новичков! Красота же: обращаешься к свойству — оно само подгрузилось! Магия, ёпта!

// 1. В контексте надо включить:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseLazyLoadingProxies();

// 2. Все навигационные свойства должны быть `virtual`:
public class Order
{
    public virtual Customer Customer { get; set; } // Виртуальное!
    public virtual ICollection<OrderItem> Items { get; set; }
}

// 3. Используем:
var order = context.Orders.Find(orderId);
var customerName = order.Customer.Name; // БАЦ! И здесь невидимый запрос в базу полетел!

Плюсы? Удобно, блядь. Пишешь код, и не паришься с Include. Минусы? А минусы — это пиздец полный. Ты даже не заметишь, как в одном методе наделаешь двадцать запросов в базу (тот самый N+1). Производительность летит в пизду, а отладка превращается в поиск иголки в стоге сена. Серьёзно, используй только если понимаешь, на что идешь, или когда похуй на перфоманс.

Так что же делать, спросишь ты? А я тебе скажу. Для большинства нормальных сценариев лучший выбор — это жадная загрузка, но с проекцией (Select).

var orderData = context.Orders
    .Where(o => o.Id == orderId)
    .Select(o => new // Говорим явно, что нам нужно
    {
        o.Id,
        CustomerName = o.Customer.Name, // Только имя заказчика
        Items = o.Items.Select(i => new { i.Id, ProductName = i.Product.Name }) // Только нужные поля
    }).FirstOrDefault();

Вот это — серебряная пуля. Один запрос, контролируемый объём данных, никаких лишних JOIN по полям, которые тебе не нужны. EF сгенерирует оптимальный SQL, и все будут довольны. Ну, почти все.