В чём разница между eager loading, lazy loading и explicit loading?
Eager loading загружает связанные данные сразу через Include(); lazy loading — автоматически при первом обращении (N+1 риск); explicit loading — вручную через Entry().Collection().LoadAsync() по требованию.
Стратегии загрузки связанных данных в EF Core
EF Core предоставляет три способа загрузки навигационных свойств: eager loading, lazy loading и explicit loading. Выбор стратегии определяет, когда и сколько SQL-запросов выполняется.
Модель для примеров
public class Blog
{
public int Id { get; set; }
public string Url { get; set; } = "";
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int BlogId { get; set; }
public virtual Blog Blog { get; set; } = null!;
public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();
}
Eager Loading — жадная загрузка
Все нужные данные загружаются одним (или несколькими при AsSplitQuery) SQL-запросом прямо в момент выполнения LINQ-запроса. Используются методы Include() и ThenInclude().
// 1 запрос с JOIN:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.AsNoTracking()
.ToListAsync();
// Доступ без доп. запросов:
foreach (var blog in blogs)
foreach (var post in blog.Posts)
Console.WriteLine(post.Comments.Count);
Плюсы: предсказуемое количество запросов, хорошо работает с AsNoTracking(). Минусы: загружает данные, даже если они не нужны; при множестве коллекций может дать декартов взрыв.
Lazy Loading — ленивая загрузка
Навигационное свойство загружается автоматически при первом обращении к нему, по отдельному SQL-запросу на каждое обращение. Требует либо прокси-классов, либо инъекции ILazyLoader.
// Включение через прокси (пакет Microsoft.EntityFrameworkCore.Proxies):
services.AddDbContext<AppDbContext>(options =>
options.UseLazyLoadingProxies()
.UseSqlServer(connectionString));
// Свойства должны быть virtual:
public class Blog
{
public int Id { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}
// Использование:
var blog = await ctx.Blogs.FirstAsync(b => b.Id == 1);
// SQL выполняется здесь, при первом обращении к Posts:
var count = blog.Posts.Count; // SELECT * FROM Posts WHERE BlogId = 1
Плюсы: простота в коде — не нужно думать о Include. Минусы: N+1, не работает с AsNoTracking() + dispose контекста, незаметно генерирует лишние запросы.
Explicit Loading — явная загрузка
Связанные данные загружаются вручную через ctx.Entry(...).Collection(...).LoadAsync() или .Reference(...).LoadAsync(), только когда это действительно нужно.
var blog = await ctx.Blogs.FirstAsync(b => b.Id == 1);
// Явная загрузка коллекции с фильтром:
await ctx.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Where(p => p.Title.Contains("EF"))
.LoadAsync();
// Явная загрузка reference:
var post = await ctx.Posts.FirstAsync(p => p.Id == 42);
await ctx.Entry(post)
.Reference(p => p.Blog)
.LoadAsync();
Console.WriteLine(post.Blog.Url); // уже загружено
Плюсы: полный контроль, можно фильтровать загружаемые данные, полезно при условной загрузке. Минусы: verbose-код, легко забыть загрузить нужное свойство.
Сравнительная таблица
- Eager: запросы при материализации, подходит для API/отчётов с известным набором данных.
- Lazy: запросы при первом обращении, удобен в прототипах, опасен в продакшне без контроля.
- Explicit: запросы по требованию с фильтрацией, идеален для условных сценариев и оптимизации.
Подводные камни
- Lazy loading + closed context — если
DbContextуже disposed (например, после выхода из using), обращение к ненагруженному навигационному свойству выброситObjectDisposedException. - Lazy loading и async — прокси EF Core не поддерживают асинхронную ленивую загрузку; при обращении к свойству внутри
asyncметода происходит синхронный блокирующий вызов. - N+1 в цикле — даже при explicit loading, если вызвать
LoadAsync()внутри цикла по сущностям, получится N запросов; лучше использовать eager с Include. - AsNoTracking и lazy loading несовместимы —
AsNoTracking()отключает трекинг, а прокси для ленивой загрузки требуют отслеживаемую сущность. - Circular navigation при сериализации — двунаправленные свойства, загруженные eager/explicit, вызывают бесконечную рекурсию в JSON.NET без
ReferenceHandler.IgnoreCycles. - Include игнорируется при Select — если после
Include()идёт.Select(), EF Core игнорирует Include и строит JOIN сам по полям проекции. - Фильтрованный Include не работает с AsSplitQuery —
.Include(b => b.Posts.Where(p => p.Published))и.AsSplitQuery()несовместимы и выбросят исключение.
Common mistakes
- Путать eager, lazy и explicit loading related data с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет eager, lazy и explicit loading related data на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.