Как реализовать механизм отслеживания изменений в ORM?
Что проверяют
Понимаете ли вы, как ORM узнаёт, что именно записать в базу, и чем на примере EF Core приходится платить за это удобство. Часто просят набросать свой механизм, а следом спрашивают, почему чтение большого списка съедает память и как это исправить.
Как отвечать
Опишите три части, которые есть в любой реализации:
- identity map: в пределах одного контекста каждой строке соответствует один объект. Словарь по ключу
(Type, id)не даёт получить две копии одной сущности; - состояние сущности:
Added,Unchanged,Modified,Deleted,Detached, как вEntityStateиз EF Core; - способ обнаружить изменения: снимок или уведомления.
Снимок это поведение EF Core по умолчанию. При загрузке сохраняются исходные значения свойств, а DetectChanges(), который SaveChanges() вызывает сам, сравнивает их с текущими и помечает изменённые поля. Уведомления обходятся без сравнения: сущность реализует INotifyPropertyChanged и INotifyPropertyChanging и сама сообщает трекеру, стратегия задаётся через HasChangeTrackingStrategy. Третий вариант, прокси с виртуальными свойствами через UseChangeTrackingProxies(), перехватывает сеттеры.
Для своей реализации добавьте Unit of Work: при коммите собрать вставки, изменения и удаления, упорядочить их с учётом внешних ключей и выполнить в одной транзакции, а конфликт параллельной записи ловить токеном конкурентности вроде rowversion.
Цену назовите сразу. Трекер держит снимок каждой загруженной сущности, поэтому для чтения используют AsNoTracking(), при пакетной вставке отключают AutoDetectChangesEnabled, а массовые правки делают через ExecuteUpdate и ExecuteDelete, которые трекер обходят.
Где спотыкаются
Пишут свой UnitOfWork поверх DbContext, который просто вызывает SaveChanges. Контекст уже и есть Unit of Work, а лишняя обёртка ничего не добавляет.
Держат один контекст на всё приложение. Трекер растёт, память не освобождается, а данные в нём устаревают.
Меняют содержимое свойства-коллекции, сохранённого через собственный конвертер значений, например List<string> в JSON. Без ValueComparer изменение внутри списка трекер не видит, и SaveChanges ничего не пишет.