Thursday, April 24, 2008

Фильм "Бесценный доллар"

Посмотрел фильм «Бесценный доллар», который выложили ребята на 7 Days. 40 минут рассказа о том, почему экономика Штатов, несмотря на колоссальный дефицит бюджета и внешний долг, живет и процветает, почему доллар в Штатах стоит больше, чем доллар во всем остальном мире, на чем основана существующая система международных финансовых взаимоотношений и как это все начиналось. А также что происходит, когда вы покупаете и продаете доллары :) Не похоже на очередную теорию заговора. Просто факты и аналитика. Хотя, с другой стороны, из меня историк и экономист никакой, поэтому если есть комментарии - милости прошу, так как хочется во всем этом сильнее разобраться. Всем рекомендую к просмотру.

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

Наверно, многие уже знают этот трюк, но для меня было новостью: если в дебаге открыть окошко Call Stack, в нем вызвать контекстное меню и включить опцию Show External Code, то в окошке будут отображаться и стек вызовов внутри системных библиотек .NET Framework.



Штука очень полезная во многих случаях, поэтому рекомендую включить, кто еще не успел. Нам, например, она очень пригодилась, когда мы разбирались, из какой же страницы все-таки происходит вызов Page_Load в базовом классе :) Оказалось, совсем не из той, о которой мы думали.

Tuesday, April 15, 2008

Entity Framework rollback: Часть 4. Пример приложения

Продолжение. Начало: Entity Framework rollback: Часть 1. Постановка задачи Entity Framework rollback: Часть 2. Варианты решения Entity Framework rollback: Часть 3. Описание реализации библиотеки

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

Для примера я решил написать простенькое консольное приложение, которое работает с моделью, построенной на всеми любимой базе данных Northwind. Для того, чтобы приложение у вас запустилось, необходимо выполнить 3 условия:

  1. установить SQL Server 2005 (наверно, можно попробовать запустить и под 2000 или 2008, но я не пробовал, поэтому советовать не буду)
  2. установить базу данных Northwind, если она у вас еще не установлена
  3. исправить строку подключения к базе данных, введя свой путь к 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. Постановка задачи Entity Framework rollback: Часть 2. Варианты решения

В данной части я хотел бы дать ссылку на рабочий пример реализации механизма отката в 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, это поможет вам в будущем.

В следующем посте я расскажу про использование данной библиотеки и дам описание коду примера, который был выложен сегодня в тестовом проекте.