CRUD и LINQ-запросы
CRUD и LINQ: читаем, создаём, обновляем и удаляем данные.
Суть: CRUD (Create, Read, Update, Delete) — четыре базовые операции над данными. В EF Core запросы пишут на LINQ — языке запросов C#, который EF переводит в SQL. SaveChanges фиксирует изменения в БД.
Имея DbContext, вы работаете с данными как с обычными C#-коллекциями: фильтруете, сортируете, проецируете. EF Core превращает эти выражения в эффективный SQL и не тянет лишнего из базы.
Четыре операции
// READ: фильтрация и проекция в DTO
var users = await _db.Users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.Select(u => new UserDto(u.Id, u.Name))
.ToListAsync();
// CREATE
_db.Users.Add(new User { Name = "Аня", Email = "[email protected]" });
await _db.SaveChangesAsync();
// UPDATE
var user = await _db.Users.FindAsync(id);
user!.Name = "Анна";
await _db.SaveChangesAsync(); // change tracker сам сделает UPDATE
// DELETE
_db.Users.Remove(user);
await _db.SaveChangesAsync();
Обратите внимание: при UPDATE мы не пишем «UPDATE» — просто меняем свойство загруженного объекта, и change tracker сам поймёт, что изменилось.
Как работает под капотом
LINQ-запрос строит дерево выражений, которое EF Core анализирует и переводит в SQL. Запрос откладывается (deferred) и выполняется только при ToListAsync()/FirstOrDefaultAsync(). Поэтому Where исполнится в БД (в SQL-WHERE), а не в памяти. Покажем LINQ-логику на Python — фильтрация и проекция работают идейно так же.
# LINQ-аналог: фильтрация, сортировка, проекция
users = [
{"id": 1, "name": "Борис", "age": 30},
{"id": 2, "name": "Аня", "age": 17},
{"id": 3, "name": "Вера", "age": 22},
]
result = sorted(
({"id": u["id"], "name": u["name"]} # Select -> DTO
for u in users if u["age"] >= 18), # Where
key=lambda d: d["name"], # OrderBy
)
for r in result:
print(r)
Попробуй сам ▶ — это в точности Where(age>=18).OrderBy(name).Select(...), только на Python. В EF Core такая цепочка превратилась бы в один SQL-запрос с WHERE и ORDER BY.
Частые ошибки
- Загружать всё в память, потом фильтровать.
ToList()доWhereвытащит всю таблицу — фильтр сработает в C#, а не в SQL. - Забывать
await. Асинхронные методы (ToListAsync) надо ждать, иначе данные ещё не готовы. - N+1 запросов. Обращение к связанным данным в цикле порождает множество запросов — нужен
Include(следующий урок).
Best practices
- Используйте асинхронные методы (с суффиксом Async) — они не блокируют поток на ожидании БД.
- Проецируйте в DTO через
Selectпрямо в запросе — тянете только нужные колонки. - Один
SaveChangesна несколько изменений — это одна транзакция, эффективнее, чем по сохранению на каждое.
Отложенное выполнение и где исполняется запрос
Ключевая идея LINQ в EF Core — отложенное выполнение. Цепочка Where(...).OrderBy(...).Select(...) не ходит в базу — она лишь строит дерево выражений, описывающее намерение. Реальный SQL генерируется и выполняется в момент материализации: при ToListAsync, FirstOrDefaultAsync, CountAsync. Пока запрос не материализован, EF может перевести все операции в один эффективный SQL и выполнить фильтрацию и сортировку в базе, не таща лишние данные в память приложения.
Отсюда главное практическое правило: стройте весь запрос до материализации. Если вызвать ToList() рано, а Where применить к уже полученному списку, фильтрация уйдёт в C# и память — вся таблица будет вытянута из базы. Разница между «отфильтровать в SQL» и «вытащить всё и отфильтровать в памяти» на больших данных — это разница между миллисекундами и секундами (или падением по памяти).
Асинхронность и change tracking при записи
Обращения к БД — это операции ввода-вывода, во время которых поток просто ждёт ответа. Асинхронные методы (ToListAsync, SaveChangesAsync) освобождают поток на время ожидания, и сервер может обслуживать другие запросы тем же ресурсом. Поэтому в веб-приложениях работу с EF Core делают асинхронной — это напрямую влияет на пропускную способность под нагрузкой. Главное — не забывать await, иначе данные ещё не готовы к моменту использования.
При записи (Create/Update/Delete) работает change tracker. Для обновления достаточно загрузить сущность, поменять её свойство и вызвать SaveChanges — EF сам сравнит с исходным состоянием и сгенерирует точечный UPDATE только изменённых колонок. Несколько изменений до одного SaveChanges попадают в одну транзакцию: либо применятся все, либо ни одного. Это и удобно, и безопасно — целостность данных гарантируется на уровне транзакции БД.
Итог: CRUD в EF Core пишут на LINQ; change tracker сам формирует UPDATE/DELETE. Дальше — связи между таблицами и оптимизация чтения.