Ответ
Entity Framework Core предоставляет три основных стратегии загрузки связанных данных (навигационных свойств). Выбор зависит от сценария использования и требований к производительности.
1. Жадная загрузка (Eager Loading)
Связанные данные загружаются одним запросом вместе с основной сущностью с помощью метода Include (и ThenInclude для цепочки).
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, и все будут довольны. Ну, почти все.