Продолжение. Начало:
Entity Framework rollback: Часть 1. Постановка задачи
В данной части я хотел бы дать ссылку на рабочий пример реализации механизма отката в EF и вкратце пояснить его.
Архив с примером можно найти здесь. К слову, пытался создать проект на code.msdn.microsoft.com, но у меня вылетело два exception'а, через которые я пройти не смог. Возможно, чуть позже выложу проект и там.
Теперь пройдемся по коду. Как я уже говорил, в качестве основы я взял код из проекта Perseus: Entity Framework EntityBag, который был написан Danny Simmons. В его блоге содержится достаточно большое и подробное описание проекта и входящих в него классов, поэтому останавливаться на этом я не буду. Расскажу лишь об особенностях своей реализации и с какими сложностями я столкнулся.
Класс ContextSnapshotManager
Проект RollbackLibrary содержит 5 классов. Основным из них является ContextSnapshotManager, который содержит методы MakeSnapshot и RollbackToSnapshot для создания точки сохранения (снимка контекста) и отката до нее:
public class ContextSnapshotManager
{
public void MakeSnapshot(ObjectContext context, string snapshotName);
public void RollbackToSnapshot(ObjectContext context, string snapshotName, ref ObjectContext newContext);
}
Метод MakeSnapshot принимает контекст и уникальное имя точки, чтобы к ней можно было обратиться в дальнейшем для отката. Метод RollbackToSnapshot принимает текущий контекст, имя точки и новый контекст, в который будет записано контекст по состоянию на момент создания точки сохранения.
Класс ContextSnapshot
Класс ContextSnapshot был практически полностью скопирован у Danny, я лишь добавил метод DetachFromContext и изменил сериализацию:
internal class ContextSnapshot
{
internal ContextSnapshot(ObjectContext context)
: this(context, context.Connection.ConnectionString);
internal ContextSnapshot(ObjectContext context, string connectionString);
public void ApplyToContext(ObjectContext oldContext, ObjectContext newContext);
private void DetachFromContext(ObjectContext oldContext);
private void SerializeSnapshot(ContextSnapshotState snapshot);
private ContextSnapshotState DeserializeSnapshot();
}
Класс является внутренним (internal), поэтому создать его экземпляр изнутри сборки не получится. Класс ContextSnapshotManager создает экземпляр класса ContextSnapshot, когда мы вызываем метод ContextSnapshotManager.MakeSnapshot. В конструкторе класса ContextSnapshot происходит создание экземпляра класса ContextSnapshotState, в который помещаются все сущности из контекста, и который затем сериализуется. При вызове метода ApplyToContext экземпляр класса ContextSnapshotState десериализуется и его содержимое при помощи дополнительной логики переносится в объект newContext. Сериализацией и десериализацией занимаются приватные методы SerializeSnapshot и DeserializeSnapshot. Еще один вспомогательный метод DetachFromContext необходим для того, чтобы сделать Detach всем сущностям перед тем, как помещать их в новый контекст. Таким образом, после вызова метода ApplyToContext (который вызывает DetachFromContext) использовать старый контекст уже нельзя. Он отжил свое и мы теперь пользуемся новым.
Сериализация
Здесь нужно сделать небольшое отступление и рассказать про метод сериализации. Для сериализации нашего снимка в данный момент используется DataContractSerializer. Это новый тип сериализаторов, который был добавлен в .NET 3.0 (если я не ошибаюсь) специально для работы с WCF. В некотором приближении можно считать, что это продвинутый XmlSerializer. Сериализирует он в свой внутренний формат XML, что означает, что мы получаем данных больше, чем в случае с BinaryFormatter'ом, но в то же время DataContractSerializer более удобен в использовании, чем XmlSerializer. Выбрал я DataContractSerializer по нескольким причинам:
- Danny использует его в своем примере, но у него работа идет как раз с WCF, у нас задача немного другая, но все равно интересно было посмотреть, что это за зверь такой
- в сгенерированный набор классов EF (тот, который генерируется по edmx-схеме) уже добавлена правильная поддержка XmlSerializer, SoapSerializer и DataContractSerializer, а между этими двумя лучше все же выбирать второй
Во втором пункте я упомянул, что в наборе классов EF есть встроенная правильная поддержка XmlSerializer, SoapSerializer и DataContractSerializer. Это означает, что для всех классов и свойств этих классов уже проставлены атрибуты DataContract и DataMember, а также XmlIgnore и SoapIgnore, причем проставлены они с умом. Navigation properties помечены как игнорируемые, то есть мы будем сериализировать лишь этот конкретный объект, а не все объекты, связанные с ним, и т.д. Это необходимо помнить, т.к. если вы в процессе работы извлекли из базы в контекст связанные объекты, то после отката они будут потеряны и их нужно извлекать заново. Мы на нашем проекте уже сделали автоматическую генерацию кода, которые реализует lazy load при первом обращении к navigation property, поэтому для нас этой проблемы не существует.
В принципе, никто не мешает изменить сериализатор на BinaryFormatter, но это требует немного больше кода. К сожалению, разработчики EF не предусмотрели установку атрибута NotSerialized для игнорирования navigation properties (почему, для меня до сих остается загадкой), а в partial классе установить его не представляется возможным. Если не игнорировать navigation properties, то вы можете столкнуться с несколькими проблемами:
- будут сериализоваться связанные объекты, что увеличит объем сериализованных данных
- при откате в некоторых случаях будет происходить exception, так как метод Attach в EF работает очень хитро: вы вызываете его для одной сущности, а приаттачатся к контексту все, с ней связанные, что приведет к ошибке логики восстановления контекста
Таким образом, остается один выход: реализация интерфейса ISerializable в partial-классах для каждого класса-сущности, что несколько трудозатратно. Конечно, можно реализовать это в одном базовом классе, в котором выяснять, что мы сериализуем, а что нет при помощи рефлексии, но тут, боюсь, мы утратим одно из основных преимуществ BinaryFormatter – скорость. Второе преимущество – размер – можно также нивелировать, если хранить сериализованные данные не в памяти (как в моем примере), а в файлах на диске.
В любом случае, право выбора сериализатора остается за вами.
Класс ContextSnapshotState
Продолжим обзор классов. Класс ContextSnapshotState выглядит очень просто:
internal class ContextSnapshotState
{
// serializable
[DataMember]
string connectionString;
[DataMember]
List<IEntityWithKey> unchangedEntities;
[DataMember]
List<IEntityWithKey> modifiedEntities;
[DataMember]
List<IEntityWithKey> modifiedOriginalEntities;
[DataMember]
List<IEntityWithKey> deletedEntities;
[DataMember]
List<IEntityWithKey> addedEntities;
[DataMember]
List<RelationshipEntry> addedRelationships;
[DataMember]
List<RelationshipEntry> deletedRelationships;
[DataMember]
List<RelationshipEntry> unchangedManyToManyRelationships;
// not serializable
Dictionary<EntityKey, int> addedEntityKeyToIndex;
...
// constructor, properties and one method
}
В классе ContextSnapshotState мы отдельно храним и сериализуем сущности и связи между ними в зависимости от текущего состояния сущности или связи. То есть у нас есть коллекции неизменных, добавленных, измененных и удаленных сущностей, а также неизмененных, добавленных и удаленных связей. Стоит еще отметить, что для измененных сущностей мы храним по 2 копии сущности: текущую и оригинальную (это нужно для правильного восстановления сущности). Отдельным полем мы сохраняем также строку подключения. Однако здесь есть одна хитрость, связанная с выбранным типом сериализации. В первую очередь, не все поля класса помечены атрибутом DataMember. Dictionary addedEntityKeyToIndex мы не сериализуем, т.к. он нам нужен лишь для построения правильных связей. Во-вторых, и это очень важно, нам нужно вручную прописать атрибуты KnownType для всех типов объектов, которые могут попасть в снимок. А это значит, что если вы добавляете новую таблицу в базу и соответственно новый класс в контекст, то нужно не забыть добавить его в список известных типов, иначе при десериализации будет exception. Выглядит это все примерно так:
[DataContract]
[KnownType(typeof(Alphabetical_list_of_products))]
[KnownType(typeof(Categories))]
[KnownType(typeof(Category_Sales_for_1997))]
[KnownType(typeof(Current_Product_List))]
...
[KnownType(typeof(Territories))]
internal class ContextSnapshotState
{
...
}
Класс RelationshipEntry
Класс RelationshipEntry призван помочь в сериализации связей между двумя любыми сущностями. Каждый экземпляр класса содержит одну связь, которая характеризуется именем связи, ее состоянием и параметрами двух концов связи. Здесь мы также сериализуем не все.
[DataContract]
internal class RelationshipEntry
{
// serializable
[DataMember]
string RelationshipName { get; set; }
[DataMember]
EntityState State { get; set; }
[DataMember]
string Role1 { get; set; }
[DataMember]
EntityKey Key1 { get; set; }
[DataMember]
int AddedEntityIndex1 { get; set; }
[DataMember]
string Role2 { get; set; }
[DataMember]
EntityKey Key2 { get; set; }
[DataMember]
int AddedEntityIndex2 { get; set; }
// not serializable
IEntityWithRelationships Entity1 { get; set; }
IEntityWithRelationships Entity2 { get; set; }
internal RelationshipEntry(ObjectStateEntry stateEntry, ObjectContext context, Dictionary<EntityKey, int> addedEntityKeyToIndex);
internal void AddRelationship(ObjectContext context, List<IEntityWithKey> addedEntities);
internal void AttachRelationship(ObjectContext context);
internal void DeleteRelationship(ObjectContext context);
void ResolveEntitiesAndKeys(ObjectContext context, List<IEntityWithKey> addedEntities);
}
Конструктор класса принимает на вход связь, контекст и наш вспомогательный индекс, о котором знает ContextSnapshotState. Методы AddRelashionship, AttachRelationship и DeleteRelationship предназначены для добавления, связывания и удаления связей. Подробнее об этом классе можно узнать в блоге у Danny.
Класс UtilityExtensionMethods
Этот класс сложно назвать классом в полной мере этого слова, т.к. он просто является контейнером для вспомогательных методов расширения (extension methods), которые активно используются для работы с EF в проекте. Класс почти полностью скопирован, за исключением двух методов в конце:
public static IEnumerable<EntityObject> GetEntities(this NorthwindEntities context, string entitySetName, EntityState state);
public static IEnumerable<Customers> GetCustomers(this NorthwindEntities context);
Метод GetEntities позволяет получить набор сущностей определенного типа entitySetName в определенном состоянии state. Метод GetCustomers получает все неизмененные, измененные и добавленные сущности типа Customer из контекста. Мне пришлось написать его, т.к. EF по умолчанию не возвращает сущности, которые были добавлены в контекст, но не были сохранены в базу данных. Также EF возвращает уже удаленные в контексте сущности, поэтому метод GetCustomers отфильтровывает их.
public static IEnumerable<Customers> GetCustomers(this NorthwindEntities context)
{
List<Customers> unfilteredItems =
(from o in context.Customers
select o).ToList();
List<Customers> items =
(from o in unfilteredItems
where o.EntityState != EntityState.Deleted && o.EntityState != EntityState.Detached
select o).ToList();
foreach (Customers addedItem in context.GetEntities("Customers", EntityState.Added))
{
items.Add(addedItem as Customers);
}
return items;
}
Для тестового примера нам будет достаточно метода GetCustomers, так как мы работаем только с сущностями типа Customer. Для более сложных случаев вам придется реализовать методы для остальных сущностей. Постарайтесь запомнить об этой особенности EF, это поможет вам в будущем.
В следующем посте я расскажу про использование данной библиотеки и дам описание коду примера, который был выложен сегодня в тестовом проекте.
No comments:
Post a Comment