Ответ
Данные материализуются в объекты приложения в момент выполнения запроса (например, при вызове ToList(), FirstOrDefault(), foreach). Этот процесс называется материализацией (Materialization).
Последовательность шагов:
- Построение и выполнение SQL: Ваш LINQ-запрос преобразуется EF Core в SQL-команду и выполняется против базы данных.
- Чтение результатов: Данные возвращаются в виде
DbDataReader. - Материализация: Строки результата последовательно преобразуются в экземпляры классов сущностей.
Наглядный пример:
// Запрос НЕ выполняется здесь — это просто построение выражения IQueryable.
var query = context.Products.Where(p => p.Price > 100);
// В ЭТОТ МОМЕНТ происходит переход данных из БД в приложение:
// 1. Генерируется и выполняется SQL: SELECT * FROM Products WHERE Price > 100
// 2. Данные читаются из потока результатов.
// 3. Для каждой строки создаётся объект Product.
var expensiveProducts = query.ToList();
Что влияет на момент и способ загрузки данных?
- Отложенная загрузка (Lazy Loading): Связанные сущности (например,
Product.Category) загружаются автоматически при первом обращении к свойству навигации, что приводит к дополнительным запросам к БД. - Жадная загрузка (Eager Loading): Связанные данные загружаются одним запросом с помощью метода
Include. Данные материализуются вместе с основной сущностью.var productsWithCategory = context.Products .Include(p => p.Category) // JOIN с таблицей Categories .ToList(); // Категория загружается и материализуется одновременно с продуктом - Явная загрузка (Explicit Loading): Вы управляете моментом загрузки связанных данных с помощью метода
Load().
Оптимизация: Для сценариев "только для чтения" используйте .AsNoTracking(), чтобы отключить отслеживание изменений и ускорить материализацию.
Ответ 18+ 🔞
Давай разберём эту тему про материализацию, а то многие её не понимают, а потом удивляются, почему приложение жрёт память и тормозит как старая телега в гору.
Смотри, есть у тебя LINQ-запрос — это просто план действий, инструкция, а не сами данные. Как рецепт борща: пока ты не пошёл на кухню и не начал варить, борща нет, есть только бумажка с текстом. Так и тут.
Вот ты пишешь:
var query = context.Products.Where(p => p.Price > 100);
Это ты просто составил рецепт: "найти все продукты, где цена больше сотни". В базу данных никто не лез, SQL не генерировал, нихуя не произошло. Просто в query лежит описание того, что ты хочешь получить.
А теперь момент истины, когда бумажный рецепт превращается в реальный борщ:
var expensiveProducts = query.ToList();
Вот тут-то всё и начинается, ёпта!
- EF Core смотрит на твой
queryи говорит: "Ага, чувак хочет товары дороже ста рублей. Щас сделаем". Он генерирует SQL типаSELECT * FROM Products WHERE Price > 100и шлёт его базе. - База отвечает: "Держи, братан, вот тебе строки". Данные приходят в виде какого-то потока (
DbDataReader), где всё — просто столбцы и значения. - И вот здесь — магия материализации. EF Core берёт каждую строчку из результата и начинает из неё лепить объект
Product. Смотрит: ага, тутId = 5,Name = "Хуевый навигатор",Price = 150. Он создаёт новый экземпляр классаProductи пихает в него эти значения. И так для каждой строки. Вуаля — у тебя в памяти теперь живая коллекция объектов, с которыми можно работать.
А теперь про самое интересное — как эта материализация дружит с загрузкой связанных данных. Тут три пути, и все ведут в разные стороны.
1. Жадная загрузка (Eager Loading) — "Загрузи всё и сразу, нахуй!"
Используешь .Include(). Это как прийти в магазин и купить не только водку, но и сразу закусь, сиги и ещё пива на дорожку. Один поход, один запрос, но тащишь всё.
var products = context.Products
.Include(p => p.Category) // Говоришь: "И категорию к каждому продукту прихвати!"
.ToList();
Что происходит? EF Core делает SQL с JOIN к таблице категорий. Когда данные приходят, он материализует и продукт, и его категорию сразу, одним махом. Всё уже связано и готово к употреблению.
2. Отложенная загрузка (Lazy Loading) — "Давай по мере надобности, но с сюрпризами" Включил эту фичу — и теперь, когда ты обращаешься к свойству-навигации, которого ещё нет в памяти, EF Core лезет в базу и догружает его.
var product = context.Products.First(); // Загрузили только продукт
var categoryName = product.Category.Name; // Опа! Тут EF Core бежит делать ещё один запрос: "SELECT * FROM Categories WHERE Id = ..."
Выглядит удобно, но может запросто устроить N+1 проблему. Представь, ты в цикле прошёлся по 100 продуктам и для каждого запросил категорию — будет 101 запрос к базе (один на продукты + 100 на категории). Приложение будет работать, как будто у него запоры, блядь.
3. Явная загрузка (Explicit Loading) — "Я сам решу, когда и что грузить" Ты явно командуешь: "Загрузи-ка мне вот эту категорию для этого продукта". Контроль полный.
var product = context.Products.First();
context.Entry(product).Reference(p => p.Category).Load();
И лайфхак на посошок: AsNoTracking()
Если ты просто читаешь данные, чтобы показать их на экране, и не собираешься их изменять и сохранять обратно — скажи об этом EF Core!
var products = context.Products.AsNoTracking().ToList();
Это как сказать: "Не следи за этими объектами, не создавай для них клонов в своём внутреннем кеше, просто дай мне данные". Материализация станет быстрее, потому что не нужно строить сложные графы отслеживания изменений. Памяти жрёт меньше. Используй, где можно.
Короче, суть в чём: данные из таблицы в объекты превращаются только в момент, когда ты их реально просишь (ToList, First, foreach). А как именно они превращаются — одним куском или по частям — зависит от твоей стратегии загрузки. Выбирай с умом, а то получишь тормоза и дикие запросы к базе.