Что произойдет при вызове SaveChanges() после изменения отслеживаемой сущности в Entity Framework Core?

Ответ

EF Core отслеживает изменения всех сущностей, полученных в контексте (с AsTracking()). При вызове SaveChanges() он фиксирует все накопленные изменения в базе данных внутри транзакции.

Что именно происходит:

  1. Анализ ChangeTracker: EF Core проверяет DbContext.ChangeTracker на наличие сущностей в состояниях Added, Modified, или Deleted.
  2. Генерация SQL: Для каждой измененной сущности генерируется соответствующий SQL-команда (INSERT, UPDATE, DELETE).
  3. Выполнение в транзакции: Все команды выполняются как одна транзакция (если не используется явная транзакция).
  4. Сохранение или откат: Если все команды выполнены успешно, транзакция коммитится, и состояния сущностей меняются на Unchanged. При ошибке транзакция откатывается, а исключение пробрасывается выше.

Практический пример:

using var db = new AppDbContext();

// Сущность теперь отслеживается контекстом
var blog = await db.Blogs.FirstAsync(b => b.Id == 1);
blog.Rating = 5; // Состояние меняется на Modified
blog.Url = "https://newurl.com";

// Добавляем новую сущность
db.Blogs.Add(new Blog { Url = "https://example.com" }); // Состояние Added

// Удаляем сущность
db.Remove(await db.Blogs.FirstAsync(b => b.Id == 99)); // Состояние Deleted

// Все три операции (UPDATE, INSERT, DELETE) будут выполнены здесь
int affectedRows = await db.SaveChangesAsync();
Console.WriteLine($"Затронуто строк: {affectedRows}");

Ключевые настройки и best practices:

  • SaveChangesAsync(): Всегда предпочтительнее синхронного варианта для не блокирующего IO.
  • Пакетная обработка (Batching): EF Core может объединять несколько команд в один пакет (roundtrip) для повышения производительности.
  • Оптимистическая блокировка: Используйте свойство с атрибутом [ConcurrencyCheck] или Timestamp, чтобы предотвратить конфликты параллельного обновления.
  • Отключение автоматического отслеживания: В сценариях только для чтения используйте AsNoTracking() для повышения производительности.

Ответ 18+ 🔞

Давай разберем, как эта штука работает, а то звучит сложно, а на деле — проще пареной репы.

Представь, что твой DbContext — это такой дотошный завхоз на складе. Каждую коробку (сущность), которую ты через него получаешь с AsTracking() (а это по умолчанию), он начинает пристально отслеживать. Записывает в свою тетрадку, что с ней делают.

А потом, когда ты говоришь SaveChanges(), он берет эту тетрадку и одним махом, в рамках одной транзакции, вносит все изменения на склад (в базу данных). Либо всё успешно применяется, либо, если где-то косяк, откатывается как ни в чём не бывало.

По шагам, что там внутри творится:

  1. Ревью тетрадки (ChangeTracker). Завхоз листает свою тетрадь и ищет пометки: что добавили (Added), что поменяли (Modified), что списали в утиль (Deleted).
  2. Пишет команды. Для каждой такой пометки он генерирует SQL-команду: INSERT, UPDATE или DELETE.
  3. Работает одной сменой (транзакция). Все эти команды он пытается выполнить как единую операцию. Это, блядь, важно — либо всё, либо ничего.
  4. Финал. Если всё заебок, транзакция подтверждается, а в тетрадке напротив этих сущностей ставится галочка "актуально". Если где-то пиздец — всё откатывается, и летит исключение.

Вот как это выглядит в коде, на живом примере:

using var db = new AppDbContext();

// Достаём блог из базы. Завхоз сразу начинает за ним следить.
var blog = await db.Blogs.FirstAsync(b => b.Id == 1);
blog.Rating = 5; // Завхоз в тетрадке: "Блог 1 — изменён (Modified)"
blog.Url = "https://newurl.com";

// Подкидываем ему новую коробку (блог). Он её принимает и помечает "новая (Added)".
db.Blogs.Add(new Blog { Url = "https://example.com" });

// Говорим списать старый блог в утиль. Пометка "удалён (Deleted)".
db.Remove(await db.Blogs.FirstAsync(b => b.Id == 99));

// А теперь — волшебный пендель! Завхоз бежит исполнять все накопившиеся поручения.
int affectedRows = await db.SaveChangesAsync();
Console.WriteLine($"Затронуто строк: {affectedRows}"); // Тут будет 3, если всё прошло.

А теперь лайфхаки, чтобы не выстрелить себе в ногу:

  • SaveChangesAsync() — твой друг. Всегда используй асинхронный вариант, если можешь. Не блокируй главный поток, ебать его в сраку.
  • Пакетная обработка (Batching). Умный завхоз может несколько мелких поручений (UPDATE, INSERT) объединить в одну пачку и сбегать на склад один раз, вместо трёх. Производительность растёт, как на дрожжах.
  • Оптимистическая блокировка. Чтобы два человека не перезаписали изменения друг другу, используй [ConcurrencyCheck] или поле с Timestamp. Это как сказать: "Если пока я редактировал, кто-то уже тронул запись — кинь мне ошибку, а не тихо её перезапиши".
  • Не отслеживай лишнее. Если ты просто читаешь данные, чтобы показать их на экране, и не собираешься менять — скажи завхозу не париться. Используй AsNoTracking(). Производительность взлетит, потому что он не будет тратить силы на ведение своей ебучей тетрадки.