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. Дальше — связи между таблицами и оптимизация чтения.

Проверьте себя
1. Почему важно ставить Where ДО ToList, а не после?
AЭто не важно
BС Where до материализации фильтр выполнится в SQL (в БД), а после ToList — уже в памяти C#
CToList ускоряет фильтрацию
DWhere не работает с базой
2. Как EF Core узнаёт, что объект нужно обновить (UPDATE)?
AНужно вызвать метод Update вручную всегда
BChange tracker отслеживает загруженный объект и при SaveChanges видит изменённые свойства
CEF обновляет всю таблицу
DЧерез миграцию