Продолжение. Начало:
В последней части этой серии я хотел бы привести описание примера клиентского приложения, которое использует приведенный механизм отката. Это приложение можно найти вместе с библиотекой, которую я выложил в предыдущей части.
Для примера я решил написать простенькое консольное приложение, которое работает с моделью, построенной на всеми любимой базе данных 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;
}
Вот, собственно говоря, и все. Берите, пользуйтесь, любые пожелания и предложения приветствуются. На данный момент этот код уже прошел проверку в реальной работе, но не факт, что в нем нет багов.

