EF Core
DbContext и DbSet, change tracker и AsNoTracking, стратегии загрузки, проблема N+1, миграции и оптимистичная блокировка.
11 вопросов
JuniorТеорияОчень частоЧто такое DbContext в объектно-реляционном маппере EF Core и что представляет свойство DbSet<T>?
Что такое DbContext в объектно-реляционном маппере EF Core и что представляет свойство DbSet<T>?
DbContext — это сессия работы с базой: он держит модель, соединение и change tracker, а SaveChanges разом отправляет накопленные правки. Каждое свойство DbSet<T> — это queryable-коллекция одного типа сущности и точка входа для Add и Remove по нему.
Типичные ошибки
- ✗Считать
DbContextпотокобезопасным и делить один экземпляр между параллельными запросами - ✗Думать, что правка отслеживаемой сущности доходит до базы до вызова
SaveChanges - ✗Воспринимать
DbSet<T>как список в памяти, а не как queryable над таблицей
Уточняющие вопросы
- →Почему
AddDbContextрегистрирует контекст со временем жизниScoped? - →Что произойдёт, если запросить одну и ту же сущность дважды через один
DbContext?
MiddleТеорияОчень частоЧто делает change tracker объектно-реляционного маппера EF Core и что пропускает AsNoTracking?
Что делает change tracker объектно-реляционного маппера EF Core и что пропускает AsNoTracking?
Отслеживаемый запрос кладёт каждую сущность в ChangeTracker со снимком её исходных значений и разрешает identity, так что один ключ даёт один экземпляр. SaveChanges сравнивает снимки с текущими значениями и строит UPDATE. AsNoTracking пропускает снимок и identity resolution — чтение дешевле, но такие сущности уже не сохранить.
Типичные ошибки
- ✗Взять
AsNoTracking, затем изменить сущность и вызватьSaveChanges, ожидаяUPDATE - ✗Думать, что отслеживание кэширует строки и повторный запрос не пойдёт в базу
- ✗Упускать, что именно identity resolution даёт один экземпляр на один ключ
Уточняющие вопросы
- →Когда
AsNoTrackingWithIdentityResolutionоправдывает свою дополнительную цену? - →Как
AttachиUpdateснова делают отсоединённую сущность сохраняемой?
JuniorТеорияЧастоКак зарегистрировать DbContext, чтобы сервис его получил, и какое время жизни он получает?
Как зарегистрировать DbContext, чтобы сервис его получил, и какое время жизни он получает?
builder.Services.AddDbContext<AppDbContext>(...) регистрирует контекст и настраивает его провайдер. Регистрация — scoped: контейнер создаёт один экземпляр на запрос и освобождает его вместе со scope. Сервис принимает AppDbContext параметром конструктора и не создаёт его через new.
Типичные ошибки
- ✗Регистрировать контекст как singleton, разделяя один change tracker между запросами
- ✗Создавать
DbContextчерезnewвместо получения из контейнера - ✗Считать, что два внедрения
AppDbContextв одном запросе — разные экземпляры
Уточняющие вопросы
- →Что ломается, когда singleton-сервис принимает scoped
DbContextв конструкторе? - →Что меняет
AddDbContextPoolв переиспользовании экземпляров?
MiddleТеорияЧастоПочему DbContext — неявная реализация паттерна управления транзакцией Unit of Work?
Почему DbContext — неявная реализация паттерна управления транзакцией Unit of Work?
Он накапливает все добавления, изменения и удаления своих отслеживаемых сущностей и фиксирует их вместе: один SaveChanges открывает транзакцию, записывает всё разом и при сбое всё откатывает. Поэтому AddDbContext регистрирует его как Scoped — один контекст на запрос означает одну единицу работы на запрос, уничтожаемую вместе со scope.
Типичные ошибки
- ✗Вызывать
SaveChangesпосле каждого изменения сущности, теряя пакет в одной транзакции - ✗Регистрировать
DbContextкакSingletonи делить одну единицу работы между запросами - ✗Считать, что неудавшийся
SaveChangesоставляет часть пакета применённой в базе
Уточняющие вопросы
- →Что происходит с состоянием change tracker, когда
SaveChangesбросает исключение? - →Когда нужна явная транзакция вокруг нескольких вызовов
SaveChanges?
MiddleТеорияЧастоЧто такое проблема N+1 в объектно-реляционном маппере EF Core и как её устранить?
Что такое проблема N+1 в объектно-реляционном маппере EF Core и как её устранить?
Один запрос загружает N родителей, а затем обращение к навигации каждого из них порождает ещё по одному запросу — N+1 обращений к базе вместо одного. Загружайте связанные данные тем же запросом: Include для всего графа или проекция Select только по нужным колонкам. Обычная причина — lazy loading, поэтому в API его отключают.
Типичные ошибки
- ✗Оставить lazy loading включённым и обходить навигации внутри цикла
- ✗Считать, что lazy loading сам складывает лишние запросы в одно обращение
- ✗Тянуться к
Include, когда проекцияSelectвытянула бы куда меньше данных
Уточняющие вопросы
- →Когда
AsSplitQueryвыигрывает у одного join черезInclude? - →Как заметить N+1 в логах или в снятой трассировке SQL?
MiddleТеорияЧастоЧем различаются eager, explicit и lazy loading при загрузке связанной сущности в EF Core?
Чем различаются eager, explicit и lazy loading при загрузке связанной сущности в EF Core?
Eager loading тянет связь внутри исходного запроса — через Include. Explicit loading подтягивает её позже по требованию, вызовом Entry(...).Collection(...).Load() — это одно лишнее обращение, о котором вы попросили сами. Lazy loading тянет её в момент чтения проксированной навигации: неявно, и это классический источник N+1.
Типичные ошибки
- ✗Путать explicit и lazy loading: первый — намеренный вызов, второй срабатывает на обращении
- ✗Оставить прокси lazy loading включёнными в API и случайно выкатить N+1
- ✗Ждать, что навигация заполнится без
Include, безLoad()и без прокси
Уточняющие вопросы
- →Каким должно быть свойство навигации, чтобы прокси lazy loading заработали?
- →Почему ленивая навигация бросает исключение после уничтожения своего
DbContext?
MiddleТеорияЧастоЧто на самом деле делает команда dotnet ef migrations add и чего она не делает?
Что на самом деле делает команда dotnet ef migrations add и чего она не делает?
Она строит модель из вашего DbContext, сравнивает её со снимком модели от предыдущей миграции и генерирует C#-класс миграции с Up и Down плюс обновлённый снимок. Базы она не касается вовсе — SQL выполнится позже, при dotnet ef database update или вызове Migrate() на старте.
Типичные ошибки
- ✗Думать, что команда трогает базу, а не просто генерирует код
- ✗Править руками или удалять снимок модели, из-за чего следующий diff даёт неверную миграцию
- ✗Ждать, что миграция попадёт в таблицу истории ещё до применения
Уточняющие вопросы
- →Почему файл снимка модели важен для diff следующей миграции?
- →Как превратить миграцию в SQL-скрипт, который посмотрит администратор базы данных?
SeniorТеорияИногдаКакую стоимость снимает скомпилированный запрос с запроса на LINQ и когда он оправдан?
Какую стоимость снимает скомпилированный запрос с запроса на LINQ и когда он оправдан?
Компилируется каждый запрос: провайдер обходит дерево выражений, вычисляет по нему ключ кэша и ищет план в своём кэше запросов. Скомпилированный запрос делает это один раз и отдаёт делегат, поэтому обход дерева и поиск в кэше исчезают из каждого последующего вызова. Он оправдан для горячих, часто исполняемых запросов фиксированной формы — структура не может меняться от вызова к вызову.
Типичные ошибки
- ✗Ждать, что скомпилированный запрос кэширует результаты, а не план запроса
- ✗Компилировать запрос, форма которого меняется от вызова к вызову
- ✗Компилировать холодные или редкие запросы и не получать никакого измеримого выигрыша
Уточняющие вопросы
- →Почему скомпилированный запрос принимает свой
DbContextпараметром делегата? - →Отслеживает ли скомпилированный запрос результаты и как снять эту стоимость?
SeniorТеорияИногдаКак столбец-маркер версии rowversion даёт оптимистичную конкурентность и что происходит при конфликте?
Как столбец-маркер версии rowversion даёт оптимистичную конкурентность и что происходит при конфликте?
Свойство настраивают через IsRowVersion(): значение, прочитанное вместе с сущностью, попадает в WHERE сгенерированного UPDATE. Если другая транзакция успела изменить строку, версия уже не совпадает, затронуто ноль строк, и SaveChanges бросает DbUpdateConcurrencyException. Дальше вы перечитываете текущие значения и повторяете, сливаете или показываете конфликт.
Типичные ошибки
- ✗Поймать исключение о конкурентности и снова вызвать
SaveChangesна той же устаревшей сущности - ✗Понимать оптимистичную конкурентность как блокировку, а не как конфликт, найденный при записи
- ✗Решать, как разрулить конфликт, не перечитав текущие значения из базы
Уточняющие вопросы
- →Как исходные значения записи и вытянутые значения из базы помогают слить конфликт?
- →Когда токеном конкурентности делают бизнес-колонку вместо
rowversion?
SeniorДебаггингИногдаЭндпоинт на запросе LINQ куда медленнее своего SQL — найдите причину
Эндпоинт на запросе LINQ куда медленнее своего SQL — найдите причину
В логе один запрос за 50 заказов и затем по запросу на строки каждого заказа — 51 обращение к базе. Это N+1 из-за навигации, разрешаемой лениво внутри цикла; каждая команда быстрая, поэтому дело не в SQL, а в их количестве. Загрузите строки тем же запросом через Include или проекцию Select, отключите lazy loading и добавьте AsNoTracking, чтобы снять стоимость отслеживания на эндпоинте только для чтения.
Типичные ошибки
- ✗Читать быстрые отдельные команды как доказательство здоровья запроса, игнорируя их количество
- ✗Винить отсутствующий индекс, когда время стоит именно число обращений к базе
- ✗Считать, что нетранслируемый
Whereтихо вычислится на клиенте, хотя с 3.0 он бросает исключение
Уточняющие вопросы
- →Какую категорию логов или диагностику вы включите первой, чтобы это увидеть?
- →Сколько ещё стоит отслеживание, когда N+1 устранён, и как вы это измерите?
SeniorДизайнРедкоИзменение схемы надо выкатить на живой API, который не прекращает обслуживание. Колонку Customers.Name нужно превратить в FullName и сделать её non-nullable. Сервис работает на нескольких инстансах за балансировщиком и раскатывается rolling-обновлением, поэтому какое-то время старая и новая сборки ходят в одну и ту же базу. В таблице десятки миллионов строк, а пайплайн применяет миграции EF Core автоматически до того, как новые инстансы примут трафик. Простой, блокировка, надолго останавливающая запись, и неоткатываемый неудачный деплой недопустимы. Спроектируйте миграцию: сколько нужно деплоев, что каждый меняет в схеме и в коде, как заполняются существующие строки и как каждый шаг остаётся обратимым.
Изменение схемы надо выкатить на живой API, который не прекращает обслуживание. Колонку Customers.Name нужно превратить в FullName и сделать её non-nullable. Сервис работает на нескольких инстансах за балансировщиком и раскатывается rolling-обновлением, поэтому какое-то время старая и новая сборки ходят в одну и ту же базу. В таблице десятки миллионов строк, а пайплайн применяет миграции EF Core автоматически до того, как новые инстансы примут трафик. Простой, блокировка, надолго останавливающая запись, и неоткатываемый неудачный деплой недопустимы. Спроектируйте миграцию: сколько нужно деплоев, что каждый меняет в схеме и в коде, как заполняются существующие строки и как каждый шаг остаётся обратимым.
Не переименовывайте на месте — сначала expand, затем contract, за три деплоя. Деплой 1 добавляет nullable FullName, пишет в обе колонки и читает Name. Старые строки заполняются пачками, чтобы не брать долгую блокировку. Деплой 2 читает FullName и всё ещё пишет в обе, когда заполнение проверено. Деплой 3 делает FullName non-nullable и удаляет Name. Каждый шаг аддитивен, работает при обеих живых сборках и откатывается сам по себе.
Типичные ошибки
- ✗Переименовывать или удалять колонку тем же деплоем, что меняет читающий её код
- ✗Заполнять десятки миллионов строк одним
UPDATE, удерживая долгую блокировку таблицы - ✗Делать новую колонку non-nullable до того, как значение появилось у каждой строки
Уточняющие вопросы
- →Как сделать каждый из трёх деплоев обратимым по отдельности?
- →Как readiness-пробник удерживает трафик от инстанса, чей шаг схемы ещё не завершён?