Ответ
В Entity Framework Core запрос строится с помощью LINQ, но выполняется в базе данных только при вызове терминального оператора (terminal operator). До этого момента строится дерево выражений (Expression Tree).
Основные категории терминальных операторов, выполняющих запрос:
-
Материализация в коллекцию: Преобразуют результат запроса в объект в памяти.
ToList(),ToListAsync()ToArray(),ToArrayAsync()ToDictionary(),ToLookup()
-
Получение одного элемента:
First()/FirstAsync(),FirstOrDefault()/FirstOrDefaultAsync()Single()/SingleAsync(),SingleOrDefault()/SingleOrDefaultAsync()Last()/LastAsync(),LastOrDefault()/LastOrDefaultAsync()(может быть неэффективным, если не отсортировано)
-
Агрегатные функции: Выполняют вычисление на стороне БД и возвращают скалярное значение.
Count()/CountAsync(),LongCount()Any()/AnyAsync()All()/AllAsync()Min()/MinAsync(),Max()/MaxAsync()Sum()/SumAsync(),Average()/AverageAsync()
-
Загрузка связанных данных (Eager Loading): Методы
Include()иThenInclude()сами по себе не выполняют запрос, но влияют на его форму. Запрос выполнится при вызове терминального оператора после них.
Пример, иллюстрирующий отложенное выполнение:
// 1. Только строится дерево выражений. Запрос в БД НЕ выполняется.
var query = context.Users
.Where(u => u.IsActive)
.OrderBy(u => u.LastName)
.Select(u => new { u.Id, u.FullName });
// 2. ЗАПРОС ВЫПОЛНЯЕТСЯ ЗДЕСЬ, при вызове ToListAsync().
var activeUsers = await query.ToListAsync();
// 3. Ещё один ОТДЕЛЬНЫЙ запрос выполнится здесь.
var count = await context.Users.CountAsync(u => u.Age > 25);
Важное замечание: Неоднократный вызов терминальных операторов на одном и том же IQueryable без AsNoTracking() может привести к нескольким запросам к БД. Для повторного использования результатов их нужно материализовать (например, в List).
Ответ 18+ 🔞
Давай разжую эту тему про терминальные операторы в EF Core, а то народ путается, как последние идиоты, и потом удивляется, почему приложение жрёт память и тормозит, будто дед на Windows 98.
Представь, что твой IQueryable — это не готовый список, а, блядь, рецепт блюда. Ты написал на бумажке: "взять пользователей, отфильтровать активных, отсортировать по фамилии, выбрать только ID и имя". Это просто текст, инструкция. Пока ты эту бумажку на кухню (в базу данных) не отнёс, нихуя не готово.
Терминальный оператор — это тот момент, когда ты этот рецепт, сука, тащишь на кухню и кричишь: "Вари!"
Пока не вызвал терминал — всё это просто дерево выражений в памяти, болтается. Вызвал — пошёл SQL-запрос, база данных вспотела и вернула тебе результат.
Какие бывают эти самые "повара", которые запускают готовку:
-
"Сделай мне полный комплект, в кастрюлю!" — материализация в коллекцию.
ToList(),ToListAsync()— самый частый гость. Всё, что накопилось в рецепте, превращает вList<T>.ToArray(),ToDictionary()— тоже самое, но в другой посуде.
-
"Мне только один кусочек, самый первый/последний/определённый!" — получение одного элемента.
First(),FirstOrDefault()— "дай первого, кто подходит под условия".FirstOrDefaultне орёт, если никого нет, а вернётnull.First()устроит истерику (InvalidOperationException).Single(),SingleOrDefault()— "должен быть ровно ОДИН такой!". Если ноль или больше одного — исключение. Используй, когда точно знаешь, что элемент уникален (поиск по ID, например).Last()— с ним осторожно, ёпта. Если в рецепте нетOrderBy, база может ебнуть мозг, пытаясь понять, кто тут "последний". Часто неэффективно.
-
"Посчитай-ка мне что-нибудь!" — агрегатные функции. Они не возвращают сущности, а сразу число или булево значение.
Count()— "сколько всего?"Any()— "а есть хоть один?" — обычно самый быстрый способ проверить наличие.Sum(),Average(),Min(),Max()— всё ясно, математика, 5-й класс.
-
"И сразу прихвати с собой его жён/детей/собак!" — жадная загрузка (
Include/ThenInclude). Сами по себе они запрос не запускают, они просто дописывают в рецепт: "и заодно притащи связанные заказы". Запустится всё вместе, когда придёт терминальный оператор.
Пример, где все и обламываются:
// Этап 1: Пишем рецепт. База данных спит, как сурок.
var query = context.Users
.Where(u => u.IsActive)
.OrderBy(u => u.LastName)
.Select(u => new { u.Id, u.FullName });
// Тут можно ещё 100 раз query.Where добавить — всё ещё рецепт!
// Этап 2: ПОНЕСЛАСЬ! ToListAsync() — это крик "КУХНЯ, РАБОТАЙ!".
// Генерируется SQL, летит в базу, возвращаются данные.
var activeUsers = await query.ToListAsync();
// Этап 3: А ЭТО УЖЕ СОВСЕМ ДРУГОЙ, ОТДЕЛЬНЫЙ РЕЦЕПТ И ОТДЕЛЬНЫЙ ЗАПРОС!
// Снова "КУХНЯ, РАБОТАЙ!", но уже по другому поводу.
var count = await context.Users.CountAsync(u => u.Age > 25);
Главная ловушка, в которую все попадают, как мухи в говно:
Если ты взял IQueryable, вызвал на нём .ToList() (получил список), а потом снова пытаешься с этого же IQueryable сделать .Count() — угадай что? Правильно, ВТОРОЙ ЗАПРОС К БАЗЕ! Потому что query — это всё тот же рецепт, а не сохранённый суп.
Что делать? Если результат нужен для повторной работы — материализуй его один раз и работай с коллекцией в памяти:
// Материализовали один раз, всё, запрос был один.
var myList = await query.ToListAsync();
// Дальше работаем с данными в памяти, без стрельбы по базе.
var count = myList.Count;
var firstUser = myList.First();
Запомни: IQueryable — это обещание. List<T> — это факт. С фактами работать надёжнее.