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

Ответ

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-запрос.

Ответ 18+ 🔞

Слушай, про эти стратегии загрузки в EF Core — это вообще отдельная песня. Как будто выбираешь, какую именно граблю себе в ногу сегодня. Вот смотри, разжуем по косточкам.

1. Жадная загрузка (Eager Loading) — Всё и сразу Ты такой: «А дай-ка я за один присест вытащу блог, посты, авторов, комментарии, лайки и ещё хуй знает что». Делается через Include и ThenInclude. Получается один здоровенный SQL-запрос с кучей JOIN.

// Берём всё, что плохо лежит, одним махом
var blogs = await context.Blogs
    .Include(b => b.Posts)          // Цепляем посты
        .ThenInclude(p => p.Author) // К каждому посту — автора
    .Include(b => b.Posts)
        .ThenInclude(p => p.Comments) // А к каждому посту ещё и комменты
    .Include(b => b.Owner)           // И владельца блога, куда ж без него
    .ToListAsync();

Когда юзать: Когда точно знаешь, что тебе понадобится вся эта кухня. Например, рендеришь страницу блога, где всё сразу видно. Плюсы: Один запрос — и ты король. По идее, быстро. Минусы: А по факту можно так наджойнить, что сервер базы тебя возненавидит. Вытащишь мегабайты данных, а используешь три поля. Это как купить весь магазин, чтобы съесть булку.

2. Явная загрузка (Explicit Loading) — Ручное управление Ты сначала берёшь главную сущность, а потом, по мере надобности, подгружаешь к ней всё остальное. Как будто ходишь по магазину отдельно за хлебом, отдельно за колбасой.

// Нашли блог
var blog = await context.Blogs.FindAsync(1);

// А теперь, сука, подгрузи посты к нему
await context.Entry(blog)
    .Collection(b => b.Posts)
    .LoadAsync();

// И владельца не забудь
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. В контексте написать:
    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-запрос!

*Когда юзать:* Для быстрых прототипов, десктопных приложений или когда похуй на производительность.
*Плюсы:* Невероятно удобно. Пишешь код, не паришься о загрузке.
*Минусы:*
- **Тот самый N+1:** Цикл по 10 блогам? Получи 11 запросов к базе. Сервер базы данных тебя проклянёт.
- Запросы летят в неожиданных местах, отлаживать производительность — тот ещё ад.
- Надо плодить виртуальные свойства и прокси-классы.

**Альтернатива — Проекции (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();

Вот это часто самое то. Контролируешь и запрос, и данные на выходе. EF Core сам оптимизирует JOIN. Меньше данных по сети, меньше работы для базы. Красота.

Итог:

  • Жадная — когда знаешь всё наперёд.
  • Явная — когда нужен контроль и условия.
  • Ленивая — для прототипов или когда deadlines горят, а похуй.
  • Проекции — часто лучший выбор для реальных сценариев. Учись ими пользоваться, а не долбить Include на 10 уровней вглубь.