EF5 Секреты метода DetectChanges — I

Секреты в том смысле, что это вещи, которые захочется знать EF гику.

Отслеживание изменений при работе с Entity Framework обычно не требует от разработчика каких-то особенных действий или размышлений. Тем не менее, отслеживание изменений и работа метода DetectChanges() является ключевым моментом и основной дилеммой между легкостью использования и производительностью. По этой причине было бы неплохо знать как именно работает метод DetectChanges(), что при этом происходит, для того чтобы принимать осознанные решения в случае модификации поведения при получении изменений, если ваше приложение ведет себя не так как задумывалось.

Все дальнейшие примеры будут основаны на следующем простом коде, использующем Code First подход. Да, следующая простая модель, помимо заказчиков, это блоги и посты.

 

Проблема определения изменений

Большинство современных приложений, я думаю, используют простые POCO объекты, которые не знают о методе своего сохранения в базу данных. При работе с POCO объектами используется алгоритм сравнения данных, основанный на первоначальном «снимке» объекта (Snapshot). Это значит, что в самом объекте POCO нет никакой логики, которая позволяла бы отслеживать изменения и уведомлять об этом контекст БД.

Для иллюстрации высказывания, рассмотрим такой вот код:

 

В этом куске кода, запрашивается единичный пост, меняется его название и затем сохраняется обратно в базу данных.

Но когда свойство Title меняется, ведь не происходит ничего особенного, потому что это простое автоматическое свойство C# класса. Нет ничего, что могло бы отследить изменение свойства в данном классе (к примеру, поля IsDirty), или же свойства в котором могло содержаться первоначальное значение. Так же нет ничего, что могло бы уведомить контекст базы данных о произошедших изменениях.

Так как SaveChanges() узнает о том, что необходимо послать в базу данных новое значение свойства Title? Контекст использует снимок данных для определения изменений и метод DetectChanges().

Работа DetectChanges

Контекст БД делает «снимок» свойств каждого объекта запрошенного из базы, так что в приведенном примере контекст записал в «снимок», что свойство Title было равно «My First Post». Когда вызывается метод SaveChanges(), тот в свою очередь автоматически вызывает метод DetectChanges(), и уже он проверяет все сущности в контексте сравнивая их со «снимками» которые были сделаны в момент чтения из базы. Если в процессе обнаруживаются измененные свойства, то EF знает, что необходимо сформировать запрос для обновления этого же свойства в базе данных. Таким образом, из примера выше, EF обнаружил различные значения в свойстве Title и создал нужное обновление для базы.

В реальности конечно, DetectChanges() делает больше вещей, чем я только что описал и львиная доля приходится на категорию «fixup».Если кратко, то этот процесс отвечает за восстановление и обновление связей между сущностями и корректировку внутреннего состояния объектов и индексов. Так что термин fixup можно перевести примерно как «восстановление связей», в таком виде я думаю использовать этот термин далее.

Для примера, представим, что изменилось не только свойство Title, но и вторичный ключ записи:

Когда вызывается DetectChanges() (в составе SaveChanges()), EF замечает что изменился ключ связи и делает примерно следующие вещи:

  • Проверяется, что новый FK сохранен в базе данных
  • Проверяется, существует ли блог с указанным номером в объекте post и связан ли с блогом он уже. Если не связан, то такая связь создается.
  • Так как класс Blog содержит ссылку на класс Post, метод DetectChanges() проверит, что соответствующие коллекции обновлены должным образом, чтобы отображать все новые изменения.
  • Произойдет обновление состояния объекта Blog, состояния свойств и состояния любых связанных объектов.
  • Будут обновлены внутренние индексы для поддержания связей первичный-вторичный ключ, а так же концептуальные null вторичные ключи.

Если в вашем контексте содержатся тысячи объектов и происходит интенсивная работа со всеми ними, то частый вызов DetectChanges() может вам дорого обойтись. Далее я расскажу, как можно попытаться решить проблемы производительности. Однако, для большинства приложений, даже когда они оперируют с большим количеством сущностей, метод DetectChanges() не является узким местом производительности и попытка оптимизировать приложение под этот метод может привести к сложно уловимым багам.

Самый важный момент, в котором контекст должен знать обо всех произведенных изменениях – это SaveChanges(). Да, это очевидный момент, но тем не менее. Если ничего не будет известно об изменениях, то метод SaveChanges не будет знать какие записи надо вставлять, обновлять или удалять. Поэтому DetectChanges() всегда вызывается в составе SaveChanges(), если только это не было отключено специально.

Кроме этого очевидного случая, контекст должен знать об изменениях и в других случаях. Например, если вы запросите статус объекта принадлежащего контексту, то он должен знать были ли изменения, чтобы выдать вам находится ли объект в состоянии Unchanged или Modified. Пример:

Свойство Title у объекта было изменено, так что закономерно ожидать, что состояние объекта будет обозначено как «Modified»… и это действительно так. Причиной такого поведения является вызов метода DetectChanges() внутри метода Entry().

При восстановлении связей (fixup) так же необходимо знать об изменениях в контексте. Данный процесс происходит много в каких случаях и одним из таких случаев является материализация объекта с помощью метода Find().

Данный пример кода запрашивает первую попавшуюся запись из блога с номером 1, что является первичным ключом. При изменении данного ключа EF попробует восстановить все связи при сохранении полученной записи обратно в базу данных. Конечно, это будет возможно, если контекст узнает об изменениях, что и происходит при вызове метода DetectChanges(). Как вы можете догадаться из теста, метод Find() так же вызывает метод DetectChanges(), благодаря чему сравнения проходят.

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

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.SaveChanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

Переопределения SaveChanges  и ValidateEntity

Как уже было сказано ранее, вызов DetectChanges() является частью реализации метода SaveChanges(), так что переопределяя этот метод для своих собственных нужд по оптимизации или еще чего такого, изменения не будут определены в автоматическом режиме. Для  некоторых это может стать источником недоумения и нескольких часов дебага, в попытке узнать, отчего же не определяются изменения в объектах, которые был модифицированы в процессе  работы программы. К счастью, при работе с DbContext такое происходит гораздо реже, так как Entry() и Entries() используемые для доступа к сущностям автоматически вызывают DetectChanges(). C ObjectContext не все так радужно.

Для своей собственной проверки объектов используется метод ValidateEntity(), который так же вызывается в базовой реализации GetValidationErrors() и SaveChanges(). Проверка объектов контекста так же проводится после вызова метода DetectChanges() и, как правило, изменения объектов во время проверки не происходит, поэтому вторичный вызов определения изменений не происходит. Если же свойства необходимо поменять, то лучше всего это сделать с помощью методов Entry() и Property(). Но об этом будет дальше.

Закономерно может возникнуть вопрос, отчего же не вызывать метод DetectChanges() в каждом методе контекста? И естественный ответ на это будет: производительность. Определение изменений по всему набору объектов в контексте будет слишком дорогим удовольствием, если вызывать его каждый раз, когда что-то поменялось в POCO объекте. Например, во время исполнения запроса управление возвращается приложению после материализации каждого объекта, т.е. очень много раз в течении запроса. Если приложение меняет что-то в процессе обработки такого объекта, тогда в теории это может повлиять на механизм восстановления связей последующих сущностей, надо будет это обрабатывать специальным образом, это может повлечь еще изменения и это может копиться как снежный ком и очень сильно уронить производительность. Получается, что вызывать автоматически при каждом изменении слишком дорого, но не автоматизировать этот процесс может приводить к неожиданному поведению и осложнять жизнь разработчикам. Поэтому были выбраны указанные методы, как наиболее общие и которые нуждаются в получении актуальных данных, но в то же время методы эти вызываются не настолько часто, чтобы создать нехватку ресурсов.

По мотивам статей Артура Викерса.

 

 

Hard’n’Heavy!

 

 

Violet Tape

Оставить комментарий