Какие виды загрузки связанных данных вы знаете в Entity Framework Core?

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

Ответ

EF Core предоставляет три основные стратегии загрузки связанных данных (навигационных свойств), каждая со своими компромиссами:

1. Жадная загрузка (Eager Loading) Данные загружаются одним SQL-запросом с использованием JOIN. Основной инструмент — методы Include() и ThenInclude().

// Загружаем блоги вместе со всеми постами и комментариями к каждому посту
var blogs = await context.Blogs
    .Include(b => b.Posts)          // Загружаем коллекцию Posts
        .ThenInclude(p => p.Author) // Загружаем свойство Author для каждого Post
    .Include(b => b.Posts)
        .ThenInclude(p => p.Comments) // Загружаем коллекцию Comments для каждого Post
    .Include(b => b.Owner)           // Загружаем ссылочное свойство Owner
    .ToListAsync();

Когда использовать: Когда вам заранее известны все связанные данные, которые понадобятся для отображения (например, для веб-страницы). Плюсы: Один запрос к БД, максимальная производительность для известного набора данных. Минусы: Может привести к избыточной выборке (SELECT с множеством JOIN), если включить больше данных, чем нужно («проблема SELECT N+1» наоборот).

2. Явная загрузка (Explicit Loading) Связанные данные загружаются отдельным запросом, уже после загрузки основной сущности.

var blog = await context.Blogs.FindAsync(1);

// Загружаем коллекцию Posts для конкретного блога
await context.Entry(blog)
    .Collection(b => b.Posts)
    .LoadAsync();

// Загружаем ссылочное свойство Owner
await context.Entry(blog)
    .Reference(b => b.Owner)
    .LoadAsync();

// Можно загрузить с фильтрацией и сортировкой
await context.Entry(blog)
    .Collection(b => b.Posts)
    .Query()
    .Where(p => p.IsPublished)
    .OrderBy(p => p.CreatedDate)
    .LoadAsync();

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

3. Ленивая загрузка (Lazy Loading) Связанные данные загружаются автоматически при первом обращении к навигационному свойству. Требует настройки:

  1. Установить пакет Microsoft.EntityFrameworkCore.Proxies.
  2. Включить в DbContext:
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    optionsBuilder.UseLazyLoadingProxies();
    }
  3. Сделать навигационные свойства virtual.
    
    public class Blog
    {
    public int Id { get; set; }
    public virtual ICollection<Post> Posts { get; set; } // virtual!
    }

// Использование var blog = context.Blogs.Find(1); var posts = blog.Posts; // Здесь выполнится SQL-запрос для загрузки Posts

*Когда использовать:* Для быстрого прототипирования или в десктопных приложениях, где задержка нескольких запросов не критична.
*Плюсы:* Удобство — не нужно думать о загрузке заранее.
*Минусы:*
- **Проблема N+1 запросов:** Обращение к свойству в цикле породит отдельный запрос для каждой итерации.
- Неочевидные SQL-запросы, сложность отладки производительности.
- Требует `virtual` свойств и создания прокси-классов.

**Проекции (Select) как альтернатива**
Часто лучший подход — использовать проекцию, выбирая только нужные поля:
```csharp
var result = await context.Blogs
    .Where(b => b.Id == 1)
    .Select(b => new BlogViewModel
    {
        Name = b.Name,
        PostTitles = b.Posts.Select(p => p.Title).ToList() // JOIN происходит здесь
    })
    .FirstOrDefaultAsync();

Это эффективнее, чем любая из стратегий загрузки, так как контролируется и форма данных, и SQL-запрос.