Посмотрел фильм «Бесценный доллар», который выложили ребята на 7 Days. 40 минут рассказа о том, почему экономика Штатов, несмотря на колоссальный дефицит бюджета и внешний долг, живет и процветает, почему доллар в Штатах стоит больше, чем доллар во всем остальном мире, на чем основана существующая система международных финансовых взаимоотношений и как это все начиналось. А также что происходит, когда вы покупаете и продаете доллары :) Не похоже на очередную теорию заговора. Просто факты и аналитика. Хотя, с другой стороны, из меня историк и экономист никакой, поэтому если есть комментарии - милости прошу, так как хочется во всем этом сильнее разобраться. Всем рекомендую к просмотру.
об информационных технологиях, программировании, путешествиях и фотографии
Thursday, April 24, 2008
Фильм "Бесценный доллар"
Tuesday, April 22, 2008
Путешествие «Щедрика»
Решил поискать вчера видео на композицию Krypteria – Carol of the Bells. Зашел на youtube.com, ввел в строке поиска и нашел множество вариантов, но только не тот, что мне был нужен. Одним из вариантов были Celtic Woman с той же мелодией, только в более классической форме. Начал слушать и понял, что эта мелодия мне знакома с детства. Такие динамически развивающиеся куплеты с коротенькими строчками есть и в одной из украинских народных песен – «Щедрик, щедрик, щедрівочка» мы пели еще на уроках музыки в школе. Ну, думаю, явный плагиат, снова наши взяли классную мелодию и положили на нее свои слова, ведь исполнителей английского «Щедрика» очень много. К тому же, это и в английском варианте, и в нашем - песня, которую поют на Рождество (Новый год). Начал разбираться и наткнулся на небольшой сайт с ликбезом. Оказывается, эта мелодия (вернее, обработка) была создана Николаем Леонтовичем, известным композитором (и не только) в начале века, получила огромную популярность на родине и была привезена в Штаты, где была впервые исполнена на сцене Карнеги Холла в Нью-Йорке в 1921 году. Затем в 1936 году Питером Выговским (или Вильховским) была написана английская версия слов, которая и получила название «Carol of the Bells». Эта мелодия и слова стали настолько популярны, что появлялась и появляется не только на радио и телевидении во время Рождественских праздников, но также в большом количестве фильмов и рекламе. Не «Jingle Bells», конечно, но тем не менее где-то рядом. Вот такая вот история украинской щедривки. Остается только закончить хорошей цитатой: «Весь світ співає українську пісню, а ми, вмикаючи телевізор або радіо, чуємо лише «Jingle Bells, Jingle Bells...»
Thursday, April 17, 2008
Show External Code

Штука очень полезная во многих случаях, поэтому рекомендую включить, кто еще не успел. Нам, например, она очень пригодилась, когда мы разбирались, из какой же страницы все-таки происходит вызов Page_Load в базовом классе :) Оказалось, совсем не из той, о которой мы думали.
Tuesday, April 15, 2008
Entity Framework rollback: Часть 4. Пример приложения
Продолжение. Начало:
В последней части этой серии я хотел бы привести описание примера клиентского приложения, которое использует приведенный механизм отката. Это приложение можно найти вместе с библиотекой, которую я выложил в предыдущей части.
Для примера я решил написать простенькое консольное приложение, которое работает с моделью, построенной на всеми любимой базе данных Northwind. Для того, чтобы приложение у вас запустилось, необходимо выполнить 3 условия:
- установить SQL Server 2005 (наверно, можно попробовать запустить и под 2000 или 2008, но я не пробовал, поэтому советовать не буду)
- установить базу данных Northwind, если она у вас еще не установлена
- исправить строку подключения к базе данных, введя свой путь к SQL Server и другие параметры
Строку подключения нужно поменять и в App.config проекта Client, и в App.config проекта Northwind. Насколько я понимаю, первая используется клиентским приложением, вторая – студией, когда вы работаете с моделью, но я могу и ошибаться.
private const string FirstSnapshot = "FirstSavePoint";
private const string SecondSnapshot = "SecondSavePoint";
В функции Main создаем контекст NorthwindEntities и экземпляр класса ContextSnapshotManager, который и будет отвечать за процедуру отката:
NorthwindEntities entities = new NorthwindEntities();
ContextSnapshotManager snapshotManager = new ContextSnapshotManager();
Ставим первую точку и выводим на консоль сообщение:
snapshotManager.MakeSnapshot(entities, FirstSnapshot);
PrintLogMessage("First save point is created");
Затем получаем коллекцию сущностей типа Customer, которые проживают в славном городе Лондоне, выводим их на экран и меняем им место жительства на не менее славный город Харьков :)
List<Customers> londonCustomers = GetCustomers(entities, "London");
PrintCustomers(londonCustomers);
// change customer's cities to Kharkov
foreach (Customers customer in londonCustomers)
{
customer.City = "Kharkov";
}
PrintLogMessage("London -> Kharkov");
PrintCustomers(londonCustomers);
Ставим вторую точку сохранения перед тем, как переселить наших заказчиков еще дальше:
// second save point
snapshotManager.MakeSnapshot(entities, SecondSnapshot);
PrintLogMessage("Second save point is created");
И переселяем их в Полтаву (а что, тоже отличный город, я там родился, им тоже понравится :)):
// change customer's cities to Poltava
foreach (Customers customer in londonCustomers)
{
customer.City = "Poltava";
}
PrintLogMessage("Kharkov -> Poltava");
PrintCustomers(londonCustomers);
После того, как мы проверили, что все заказчики переселились в Полтаву, мы сделаем первый откат. Откатываться будем сначала до второй точки, хотя можно и сразу до первой, если это необходимо. Прелесть тех снимков контекста, которые мы делаем в том, что нам становится не очень важно, куда откатываться. Результат выводим на консоль. Как видите, заказчики прыгнули в прошлое и снова оказались в Харькове:
// rollback to the second save point
entities = RollbackToSavePoint(snapshotManager, entities, SecondSnapshot);
PrintLogMessage("Rolling back to the second save point");
// after rollback to the second save point we should get customers in Kharkov
// we have to get customers list again because the previous list is outdated
londonCustomers = GetCustomers(entities, "Kharkov");
PrintCustomers(londonCustomers);
Здесь очень важным моментом является то, что после отката нам нужно получить коллекцию сущностей еще раз, так как после сериализации и десериализации мы получаем совершенно новый контекст с новыми объектами, а старый контекст и объекты становятся устаревшими. Будьте внимательны, если после отката у вас остаются ссылки на сущности из старого контекста – они не будут работать, как раньше, о них ничего не знает новый контекст. Для себя мы решили эту проблему, создав событие OnRolledBack, на которое подписываются все долгоживущие объекты, которые так или иначе хранят ссылки на сущности контекста. В этом событии мы просто освежаем ссылки, вытаскивая объекты из базы еще раз. Да, немного накладно, но в принципе терпимо, если этих объектов немного.
Дальше делаем второй откат, теперь уже до первой точки и выводим на консоль то, что получилось:
// rollback to the first save point
entities = RollbackToSavePoint(snapshotManager, entities, FirstSnapshot);
PrintLogMessage("Rolling back to the first save point");
// after rollback to the first save point we should get customers in London
// we have to get customers list again because the previous list is outdated
londonCustomers = GetCustomers(entities, "London");
PrintCustomers(londonCustomers);
Вот так и закончились невероятные приключения англичан в Украине. Они вернулись в Лондон и даже не помнят, что видели здание Госпрома и Поле Полтавской битвы.
Отдельного упоминания заслуживают несколько методов. Метод RollbackToSavePoint производит процедуру отката к точке snapshotName. В нем мы создаем новый объект контекста, в который во время отката переливаются сохраненные сущности, вызываем процедуру отката и затем возвращаем ссылку на новый контекст:
static NorthwindEntities RollbackToSavePoint(ContextSnapshotManager snapshotManager, NorthwindEntities currentEntities, string snapshotName)
{
// create new context
ObjectContext newEntities = new NorthwindEntities();
// rolblack to save point (snapshot)
snapshotManager.RollbackToSnapshot((ObjectContext)currentEntities, snapshotName, ref newEntities);
// restore the status quo
return (NorthwindEntities)newEntities;
}
Метод GetCustomers имеет два перегруженных варианта. Первый из них просто возвращает все сущности типа Customer, а второй возвращает лишь тех, кто проживает в определенном городе:
static List<Customers> GetCustomers(NorthwindEntities entities)
{
var result =
(from o in entities.GetCustomers()
select o).ToList<Customers>();
return result;
}
static List<Customers> GetCustomers(NorthwindEntities entities, string city)
{
var result =
(from o in entities.GetCustomers()
where o.City == city
select o).ToList<Customers>();
return result;
}
Вот, собственно говоря, и все. Берите, пользуйтесь, любые пожелания и предложения приветствуются. На данный момент этот код уже прошел проверку в реальной работе, но не факт, что в нем нет багов.
Monday, April 7, 2008
Entity Framework rollback: Часть 3. Описание реализации библиотеки
Продолжение. Начало:
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, это поможет вам в будущем.
В следующем посте я расскажу про использование данной библиотеки и дам описание коду примера, который был выложен сегодня в тестовом проекте.
Sunday, March 30, 2008
Entity Framework rollback: Часть 2. Варианты решения
В прошлом посте мы определили сценарий, для которого нужно делать откат, теперь разберемся с вариантами решения проблемы.
Для случая, когда нам просто нужно сделать откат до состояния в базе данных есть несколько вариантов решения:
- освободить и уничтожить текущий контекст, для дальнейшей работы создать новый
- воспользоваться методами 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 позже в зависимости от того, что выбрал пользователь
Saturday, March 29, 2008
Entity Framework rollback: Часть 1. Постановка задачи
Этим постом я бы хотел начать серию сообщений о реализации отката (rollback) контекста в Entity Framework (EF). Я столкнулся с этим на своем текущем проекте и хотел бы поделиться тем, что у меня получилось и, возможно, получить дельные советы от тех, кто это реализовал по-другому или у кого есть идеи по улучшению реализации.
Итак, начнем с того, что на данный момент в EF (beta 3) нет полноценной возможности сохранять изменения, которые содержаться в контексте в памяти и потом откатываться к ним. Есть варианты отката изменений из контекста до текущего состояния базы данных, но и только.
Теперь постановка задачи: представьте себе приложение ASP.NET или WinForms, в котором пользователь может взять какой-нибудь набор связанных или не связанных объектов (Поставщики, Заказы, пр.), отредактировать эти объекты и потом нажать кнопочку Save, которая сохранит данные в базу данных. Или не нажать эту кнопочку, а нажать вместо нее Cancel, что отменит изменения. Ничего сложного, скажете вы. Можно: а) дать пользователю ввести все изменения, нигде их не сохраняя, а потом просто занести в базу по нажатию кнопки, б) если же все же нужно сохранять промежуточные изменения (допустим, если нужно добавить несколько объектов подряд, или нескольких объектов в разных диалогах), то их можно сделать в контексте в памяти, а потом по нажатию кнопки одним махом сохранить или не сохранить в базу. Возражения правильные и предложенные решения работают. Однако существует еще один сценарий, который не покрывается этими двумя вариантами. По этому сценарию пользователь делает промежуточные изменения, переходит к следующим, снова делает какие-то промежуточные изменения, а потом решает откатиться до точки между этими изменениями. Например, он открыл окно редактирования объекта, увидел список связанных объектов, потом добавил пару объектов в список (первое промежуточное изменение), потом кликнул на кнопке редактирования одного из объектов из списка и получил новое диалоговое окно, в котором находится список связанных объектов, где он также может добавить несколько объектов (второе промежуточное изменение) и нажать Cancel. По нажатию на Cancel мы должны откатить второе промежуточное изменение и выйти на точку, где у нас есть только первое изменение. А вот уже эта задача, к сожалению, не решается стандартными средствами EF. Вы не можете откатиться до произвольной точки в памяти, вы можете лишь откатиться до состояния базы данных. Зато эта задача решается с помощью терпения и напильника :)
Перед тем, как идти дальше, нужно разобраться в терминологии и предмете. Контекст в EF является отсоединенным источником данных (как DataSet или DataTable). То есть по-хорошему он кэширует однажды полученные из базы данных результаты и в дальнейшем по запросам лишь проверяет, нет ли новых записей (подробнее об этом механизме я, возможно, расскажу после того, как закончу текущую серию). Таким образом, если мы выбрали данные в контекст, то в грубом приближении мы можем считать, что после этого мы работаем лишь с данными, которые находятся в памяти. Повторяю: это не так в реальности, но на данном этапе нам этого хватит. Изменения, произведенные в памяти, не сохраняются в базу до тех пор пока мы явно не вызовем метод SaveChanges у контекста. Понимание этого факта необходимо для дальнейшего разбора.
Кому интересно узнать больше про EF уже сейчас, отправляю сюда:
The ADO.NET Entity Framework (MSDN)
ADO.NET Entity Framework (Wikipedia)