В прошлом посте мы определили сценарий, для которого нужно делать откат, теперь разберемся с вариантами решения проблемы.
Для случая, когда нам просто нужно сделать откат до состояния в базе данных есть несколько вариантов решения:
- освободить и уничтожить текущий контекст, для дальнейшей работы создать новый
- воспользоваться методами Refresh у контекста для того, чтобы обновить конкретные сущности в контексте или коллекцию сущностей
Первый вариант хорош тем, что не нужно ни о чем задумываться, но немного плох с точки зрения производительности. Мало того, что будет потрачено время на создание нового контекста, так в него нужно еще и все данные загрузить заново для работы. К слову сказать, в ADO.NET team blog выложено несколько статей по анализу производительности в EF. Вы можете обратиться к ним и узнать некоторые подробности (на данный момент вышло три статьи, но думаю, будут еще):
Exploring the Performance of the ADO.NET Entity Framework - Part 1
Второй вариант предпочтителен тогда, когда вы точно знаете, что у вас обновлялось. В этом случае вы получите всего лишь несколько запросов к базе данных для тех сущностей, которые нуждаются в откате, а остальные данные останутся нетронутыми. Для подобного отката необходимо вызвать соответствующий метод Refresh и передать ему значение RefreshMode.StoreWins в качестве первого параметра. К слову сказать, если вы не знаете, что у вас менялось, то вы все равно можете реализовать этот вариант при помощи дополнительной логики приложения. Для этого нужно обратиться к контексту (вернее, к ObjectStateManager этого контекста) и узнать у него, какие сущности и связи были модифицированы, добавлены или удалены. Далее нужно обновить лишь модифицированные/удаленные сущности/связи и удалить добавленные из контекста. Совсем кошмарным с точки зрения производительности будет вариант, если вы просто пройдетесь по всем коллекциям объектов и связям без предложенной оптимизации и начнете обновлять их все подряд, поэтому в таком случае уж лучше воспользоваться первым вариантом.
Но вернемся к нашим баранам. Для нашего сценария также есть несколько вариантов решения:
- отслеживать все изменения в сущностях при помощи событий контекста
- сохранить копию контекста или его внутреннего содержимого в точке сохранения, а потом откатиться до этой точки, восстановив контекст из копии
Эти два варианта были подтверждены Danny Simmons, который является разработчиком куска EF и активным действующим лицом на форуме EF. Также у этого парня есть замечательный блог, в котором он освещает огромное количество вопросов, связанных с EF. Именно благодаря проделанной им работе и постам на форуме мы и смогли решить проблему, с которой столкнулись.
Первый вариант решения можно реализовать при помощи имеющихся в EF событий, которые срабатывают на различные операции изменения, например, EntityObject.PropertyChanging, EntityObject.PropertyChanged (модификация сущностей), RelatedEnd.AssociationChanged (модификация связей, таких как EntityReference и EntityCollection), ObjectStateManager.ObjectStateManagerChanged (добавление/удаление сущности) и др. Этот вариант хорош, но он требует очень много кодирования и анализа, так как нам нужно не просто повесить обработчики на все сущности и связи, а потом сохранить все изменения, но и откатить их корректно и в правильной последовательности. То есть если мы «удалили» сущность из контекста (а фактически, пометили ее, как удаленную), то нам нужно потом добавить ее в контекст и пометить как Unchanged. В то же время, с точки зрения производительности и расхода памяти этот вариант является более приемлемым, чем второй.
К слову сказать, Danny в своем ответе мне дал несколько дельных советов тем, кто решит реализовать именно этот вариант.
Мы же в дальнейшем будем рассматривать второй вариант, потому что он является более простым и быстрым в реализации, но в то же время нельзя сказать, что он сильно проигрывает по производительности первому. Таким образом, у нас будет следующая последовательность шагов:
- в точке сохранения сериализуем контекст
- храним сериализованный контекст до момента, когда пользователь захочет откатиться
- восстанавливаем состояние контекста из сериализованных данных
В основе этого решения частично лежит код, написанный Danny Simmons и выложенный в открытый доступ в рамкам проекта Perseus: Entity Framework EntityBag. В этом проекте Danny показал, как можно сериализовать состояние контекста (сам класс ObjectContext не сериализуем), передать его через WCF на другой хост, там сделать изменения в контексте, и вернуть данные назад. Кому интересно углубиться во внутренности EF, у вас есть такая возможность, Danny сделал превосходную серию постов, посвященную своему классу EntityBag с полным описанием шагов и подробностей реализации. У нас несколько иная задача, поэтому я использовал лишь ту часть кода, который необходим для сериализации контекста и его восстановления. Вдобавок мне пришлось реализовать дополнительный класс-менеджер точек сохранения и немного поменять метод сериализации.
PS. Есть еще два потенциально возможных решения, но мне они кажутся сложными в реализации, не очень надежными и не отвечающими поставленной задаче, поэтому мы их не рассматриваем:
- сохранять все изменения в базу данных, но помечать записи особым образом (как минимум, нужно хранить состояние каждой записи и пользователя, который сделал изменение), чтобы потом можно было откатиться
- в точке сохранения стартовать транзакцию и делать commit/rollback позже в зависимости от того, что выбрал пользователь