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

«Какие способы загрузки связанных данных (dependent entities) существуют в Entity Framework?» — вопрос из категории Entity Framework, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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();