Ответ
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) Связанные данные загружаются автоматически при первом обращении к навигационному свойству. Требует настройки:
- Установить пакет
Microsoft.EntityFrameworkCore.Proxies. - Включить в
DbContext:protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseLazyLoadingProxies(); } - Сделать навигационные свойства
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-запрос.