Dec 25, 2009

Fluent NHibernate. Маппинг наследования

Гугл подсказал мне, что блог находят в основном при поиске того, как замапить различные виды наследования используя Fluent Nhibernate. Попробую осветить этот вопрос.

Nhibernate поддерживает 3 способа реализации наследования. Различаются они количеством необходимых для этого таблиц. Далее будут рассмотрены 2 из них подробнее.

Table per class hierarchy

В данной схеме для всей иерархии классов используется одна таблица. Рассмотрим пример со следующей моделью:

image

Для хранения такой структуры будет достаточно одной таблицы с полями Id, Name, UploadDate, WhoIsOnPicture, Lenght, Type. Стоит обратить внимание на то, что поле Type используется Nhibernate’ом для того, чтобы узнать какой конкретный тип находится в конкретной строке. Итак маппинг будет выглядеть следующим образом:

public class ContentMapping: ClassMap<Content>
{
    public ContentMapping()
    {
        Id(x => x.Id).GeneratedBy.Native();
        Map(x => x.Name).Length(200).Not.Nullable();
        Map(x => x.UploadDate).Not.Nullable();
        DiscriminateSubClassesOnColumn("ContentType");
    }
}

public class PhotoMapping : SubclassMap<Photo>
{
    public PhotoMapping()
    {
        Map(x => x.WhoIsOnPicture);
        DiscriminatorValue("Photo");
    }
}

public class VideoMapping: SubclassMap<Video>
{
    public VideoMapping()
    {
        Map(x => x.Length);
        DiscriminatorValue("Video");
    }
}

Генерируемый XML довольно громоздкий, и полностью приводить его я не буду. Важно лишь то, что сформирован маппинг, и в нем используются теги subclass для photo и video.

Недостатком данной схемы является то, что в таблице Content колонки WhoIsOnPicture и Length обязательно должны позволять сохранять NULL.

Table per subclass

Очень радует, что для того, чтобы использовать этот подход, достаточно просто убрать упоминания Discriminator из классов, маппинг будет абсолютно таким же:

public class ContentMapping: ClassMap<Content>
{
    public ContentMapping()
    {
        Id(x => x.Id).GeneratedBy.Native();
        Map(x => x.Name).Length(200).Not.Nullable();
        Map(x => x.UploadDate).Not.Nullable();
    }
}
public class PhotoMapping : SubclassMap<Photo>
{
    public PhotoMapping()
    {
        Map(x => x.WhoIsOnPicture);
    }
}
public class VideoMapping : SubclassMap<Video>
{
    public VideoMapping()
    {
        Map(x => x.Length);
    }
}

Этот маппинг приведет к использованию joined-subclass элементов.

Dec 23, 2009

Проблема наследования в Nhibernate

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

Диаграмма классов с наследованием

Схема базы будет сгенерирована автоматически, если убрать комментарий со строки в Global.asax (ExposeConfiguration(BuildSchema)).

Код, который пытается изменить тип контента может выглядеть так:

var photo = Global.CurrentSession.Get<Photo>(2);
var video = new Video();
video.FromPhoto(photo);

Global.CurrentSession.Evict(photo);

Global.CurrentSession.SaveOrUpdateCopy(video);

Где метод FromPhoto выглядит так:

public virtual void FromPhoto(Photo photo)
{
    this.Id = photo.Id;
    this.Name = photo.Name;
    this.UploadDate = photo.UploadDate;
}

В общем у меня не получается заставить хибернейт изменить тип контента, возможно кто-то поможет?

Исходный код

Dec 22, 2009

Когда использовать наследование в ORM средствах

NHibernate поддерживает три способа имитации наследования в реляционных базах данных. Table per class hierarchy, table per subclass, table per concrete class. О каждой из этих стратегий можно найти множество примеров как их реализовывать. Я же хотел бы поделиться своим опытом о том, когда же стоит применять наследование.

Вплоть до моего последнего проекта, в моих базах содержались таблицы называющиеся на подобие ContentType и содержащие две колонки – Id, TypeName. Необходимость таких таблиц с точки зрения базы действительно необходимо. Вот к примеру на сайтах социальных сетей можно загружать фото и видео. У обоих есть общие атрибуты, такие как название, подпись, комментарии, дата добавления и т.д. Естественно, что будет логично расположить информацию о фото и видео в таблице Content и добавить поле Type, чтобы приложение могло отличить фото от видео (хотя бы для того, чтоб правильно отображать каждое из них).

Для ситуации, когда вариантов таких типов всего два – в общем то не будет сложностей с управлением ими. Но предположим, что вам необходимо где либо (пусть даже на странице администратора) показать список всех загруженных пользователями фотографий и видео, при этом должна быть колонка, которая в текстовом виде показывает чем является контент. Большинство сайтов в данный момент требует локализации, и простенькая таблица, в которой раньше было только два поля превращается вот в такого монстра:

Схема базы с локализацией

Если же перенести знание о типе контента в приложение, то вы можете создать следующую иерархию классов:

Диаграмма классов с наследованием

Абстрактный класс Content содержит абстрактное поле ContentType, и если же объединить этот подход с локализацией, которую я описывал ранее, то можно получить чистую доменную модель, и базу, лишенную необходимости в сложной структуре из-за локализации (как Вы сами понимаете, в базе при использовании стратегии per class hierarchy будет содержаться лишь одна таблица Content, в которое будет поле ContentTypeId, но семантика этого TypeId будет содержаться в приложении, и NHibernate сам позаботится о том, чтобы поставить для вас правильный тип).

Конечно же можно оставить в базе данных таблицу ContentType, и при выводе для пользователя проверять его значение и доставать по ключу из ресурсов нужную локализованную строку. Но при добавлении нового типа контента, я могу дать 90% гарантию, что кто-то забудет добавить строку в ресурсы, которая будет соответствовать новому типу. Или же переименование типа в базе данных приведет к полной неработоспособности приложения, но узнать об этом можно будет лишь на этапе выполнения, а не компиляции (именно это и случалось довольно часто в последнем моём проекте).

Такой подход дает еще одно преимущество – уменьшение объема (а по хорошему и полное отсутствие) условной логики в приложении. Все действия, которые зависят от того, какой тип Вам пришел можно располагать в самих конкретных объектах, а не в виде switch case конструкций. Например предположим, что с некоторых пор загрузка фото и видео в вашем приложении стала платной, причем загрузка видео стоит больше нежели загрузка фото, при отсутствии иерархии вам бы пришлось написать следующий код:

public double GetPrice()
{
    switch (ContentType)
    {
        case "Photo":
            return PHOTO_PRICE;
        case "Video":
            return VIDEO_PRICE;
        default:
            return 0;
    }
}

Не важно что будет в этом switch – будь то строки, или какой то enum. Важно, что такая конструкция будет не одна, а будет она везде где необходимо выполнить что-то в зависимости от типа контента. Как искать все условные операторы, если к фото и видео добавились еще и музыкальные файлы? Самое плохое то, что опять же ошибку вы сможете найти только во время выполнения, и самое страшное, что если забыть дописать в этом switch взимание платы за музыкальные файлы – пользователи будут загружать их по нулевой цене!

Так что всякий раз когда вы создаете таблицу с именем <Entity>Types или что-то подобное – подумайте не стоит ли заменить её полноценной схемой наследования.

Но есть одна проблема, которую мне так и не удалось решить. Возможно она специфична для NHibernate, но мне так не кажется. Если существует функционал изменения типа объекта. Например пользователь захочет наложить на фотографию музыку, и это будет уже видео. ORM не даст так просто изменить тип объекта, который привязан к определенному Id. Этот вопрос возникал и в Google groups, и я его задавал на StackOverflow.

Почему Вы не можете удалить фото, и создать вместо него объект видео? Это объясняется следующим, вероятней всего для просмотра контента вы будете использовать url наподобие www.mysite.com/Content?Id=2. И поле Id будет уникальным для контента (либо авто инкрементное поле в базе, либо hilo), значит Id вновь созданного объекта будет другим. Картинка пользователя пользовалась огромной популярностью, но пользователи, которые воспользуются старой ссылкой уже не найдут интересующее.

Так же проблемой могут быть каскадные операции. Удаление контента просто обязано повлечь за собой удаление всех комментариев к нему, но их нельзя удалять потому как они должны перейти к вновь созданному видео.

Предложенным решением является использование композиции вместо наследования. Возможно, но я ничего не могу об этом сказать, не пробовал.

В качестве заключения – создание иерархии значительно облегчило поддержку приложения для моей команды в последнем проекте, но в другом усложнило не в меньшей степени (в частности из-за проблемы упомянутой выше).

P.S. если кто-то найдет решение вопроса со сменой типа – огромная просьба поделиться :)

Dec 21, 2009

Автоматизируем изменения в Setup Project

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

Для автоматизации внесения изменений я использовал две утилиты. MsiDiff и MakeMsi.

MsiDiff позволяет сравнивать два msi файла, и генерирует для вас скрипт изменения. Этот скрипт можно выполнить при помощи утилиты MakeMsi, которая имеет множество возможностей по созданию и изменению msi файлов.

Использовать MsiDiff довольно просто. Для этого вносим все нужные для нас изменения в MSI файл с помощью любой утилиты редактирования (я использовал orca). Далее проводим сравнение двух файлов – исходного и измененного. Кстати тут есть пошаговое описание процесса.

Для моей задачи было необходимо изменить тип для Cusom Action. Сгенерированный скрипт изменения выглядел следующим образом:

#define MsiBEFORE     .\Template.msi

#include "OpenMsi.MMH"

<$Msi "<$MsiBEFORE>">
<$Table "CustomAction">
   #(
       <$Row
           @Where="`Action` = '_8126FDDA_EDC6_4CC9_9CDC_A26F93BA0A18.commit'"       
             Type="1537"      ;;Previous value = 3585
       >
   #)

<$/Table>

<$/Msi>

Этот скрипт не сам исходный вариант, сгенерированный, но он близок к нему. К сожалению выполнить этот скрипт не удается… Ошибка, которую показывает MakeMsi говорит следующее:

> MSI ERROR #2235 (see the Windows Installer documentation)
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> Column 'ExtendedType' not present or ambiguous in SQL query: SELECT
`Action`,`Type`,`Source`,`Target`,`ExtendedType` FROM CustomAction WHERE
`Action` = '_8116FDDA_EDC6_4CC9_9CDC_A16F93BA0A18.commit' (in
".\result\Result.msi")

Чтобы разобраться с этой ошибкой пришлось задать вопрос в яху группах, и ответ был получен. Поле ExtendedType было добавлено начиная с версии windows installer 4.5, а сетап проекты строятся для версии 3.1. В общем чтобы скрипт выполнялся, пришлось добавить следующее:

#define TABLES_DEFINE.CustomAction.ExtendedType N

Теперь скрипт выполняется без ошибок. Для полной автоматизации требуется возможность запустить процесс изменения msi файла не через интерфейс, а через bat файл. Если MsiDiff правильно установлен, то можно использовать следующую команду:

MM.CMD <fileName>

где fileName путь к вашему скрипту. В проекте данные изменения выполняются каждый раз как часть сборки нового билда с помощью Cruise Control .NET (как настроить).

Имперсонализация в Setup Projects

Данный пост покажет не только само решение проблемы, но и путь поиска этого решения. Возможно именно этот путь окажется более полезным, нежели само решение.

Для выполнения кода разработчика в Setup projects используются Custom Actions. Особенностью является то, что процесс установки запускается от имени пользователя SYSTEM. И поэтому Ваш код так же выполняется от имени SYSYTEM.

В одном из проектов было требование, чтобы приложение автоматически запускалось после завершения установки. Setup project не позволяют отловить нажатие кнопки Finish в конце установки. Наиболее позднее событие, доступное для обработки это Commit. Оно вызывается в момент фиксации успешной установки (визуально это происходит когда прогресс бар дошел до конца). Для запуска приложения я использовал Process.Start. Но приложение запускалось от имени SYSTEM. Из-за этого ему были не доступны настройки прокси сервера из браузера, а они были необходимы для работы.

Требовался способ запустить установку от имени текущего пользователя. Поиск велся в направлении Impersonation in setup projects.

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

You'd revert to running impersonated by editing the MSI file with the tool Orca in the Windows SDK and turning off the msidbCustomActionTypeNoImpersonate bit in the CustomAction table entries for your custom action, in the Type value.

В целом это позволило сделать вывод, что имперсонализацию, можно включить, сбросив флаг msidbCustomActionTypeNoImpersonate, дальнейший поиск велся именно в этом направлении.

В итоге чтобы включить имперсонализацию, необходимо сделать следующее:

  1. Установить утилиту orca
  2. Открыть с её помощью Ваш msi файл
  3. В левой колонке найти раздел CustomAction и выбрать его
  4. В правой части найти необходимый Custom Action
  5. Найти необходимый вам Action (в именах действий будет предшествовать некоторый Guid)
  6. Из цифры Type, соответствующей нужному действию вычесть 2048 (на самом деле этим выставляется в 0 определенный бит)
  7. Заменить Type на полученный результат
  8. Сохранить

Теперь действие будет выполняться от пользователя, запустившего установку. Огромная проблема, что действие это придется выполнять после каждой сборки. В следующем посте я опишу как автоматизировать процесс изменения msi файлов.

Dec 7, 2009

Не работает USB мышь на windows 7

В 7рке была у меня одна проблема – usb мышь после спящего режима не всегда включалась. Решил вопрос следующим образом:

Панель управления –> оборудование и звук

Панель управления

В разделе электропитание выбираем “Настройка перехода в спящий режим”

Электропитание

Далее выбираем “Изменить дополнительные параметры питания”

Дополнительные параметрыВ появившемся окне раскрываем пункт “Параметры usb”. И в значении “Параметр временного отключения USB-порта” выбираем “Запрещено”

Usb параметры  Теперь мышь включается без проблем при выходе из спящего режима.

Dec 3, 2009

Явные роли – валидация

В тему предыдущего поста о явных ролях в приложениях можно рассмотреть вопрос валидации. Каждый объект может быть валдиным или нет в зависимости от сценария, в котором он участвует в данный момент. Например правило – пользователь может стать избранным, если он сделал 6 и более заказов. Там приложение контролировало это в самом методе MakePreferred и выбрасывало исключение, если пользователь не удовлетворяет условию.

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

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

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

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

public void btnMakePreferred_Click(Object sender, EventArgs e)
{
    Customer customer = ORM.Get<Customer>(customerId);
    if(customer.Orders.Count > 5)
    {
        customer.MakePreferred();
    }
}

И это хорошо если он выглядит именно так, т.е. MakePreferred вызывается у самого объекта, тем самым давая возможность относительно легко изменить правило определения избранного пользователя. А иногда бывает и такое:

public void btnMakePreferred_Click(Object sender, EventArgs e)
{
    Customer customer = ORM.Get<Customer>(customerId);
    if(customer.Orders.Count > 5)
    {
        customer.IsPreferred = true;
    }
}

По собственному опыту я знаю что такой код встречается в проектах слишком часто.

Что же можно сделать, чтобы явно указать, что для валидации в данном контексте должно использоваться определенное бизнес правило? Ответ прост – сделать роли явными.

Для этого выделим следующий интерфейс (что из себя представляют Entity и IRole описано в предыдущем посте):

public interface IValidator<TEntity, TRole> where TEntity: Entity
                                            where TRole:   IRole
{
    bool IsValid(TRole entity);
}

Он отвечает на единственный порос, походит ли объект для данной роли. И рассмотрим конкретную реализацию:

public class MakeCustomerPreferredValidator: IValidator<Customer, IPreferredMaker>
{
    public bool IsValid(IPreferredMaker entity)
    {
        Customer customer = entity as Customer;
        if(customer == null)
        {
            throw new ArgumentException("Entity must be Customer", "entity");
        }

        return customer.Orders.Count > 5;
    }
}

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

Теперь о том как использовать этот класс, первый вариант (плохой):

IValidator<Customer, IPreferredMaker> validator = new MakeCustomerPreferredValidator();
if (validator.IsValid(preferredMaker))
{
    preferredMaker.MakePreferred();
}

Плохой потому что в этом случае, существует явная зависимость от типа MakeCustomerPreferredValidator. Мне больше нравится для этих целей некоторый Factory класс, который может быть реализован следующим образом:

public class ValidatorsFactory
{
    public IValidator<TEntity, TRole> GetValidator<TEntity, TRole>() where TEntity : Entity
                                                                     where TRole : IRole
    {
        var validators = from t in Assembly.GetAssembly(typeof(ValidatorsFactory)).GetTypes()
                         where t.GetInterfaces().Contains(typeof(IValidator<TEntity, TRole>))
                         select t;

        return (IValidator<TEntity, TRole>) validators.SingleOrDefault();
    }
}

Но я бы рекомендовал использовать любой Service Locator для этих целей.

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

Repository<Customer> customerRepository = new Repository<Customer>(Global.CurrentSession);
ValidatorsFactory factory = new ValidatorsFactory();

IValidator<Customer, IPreferredMaker> validator = factory.GetValidator<Customer, IPreferredMaker>();
IPreferredMaker preferredMaker = customerRepository.Get<IPreferredMaker>(101);

if(validator.IsValid(preferredMaker))
{
    preferredMaker.MakePreferred();
}

Такой подход обеспечивает явное разделение концепций в приложении, и дает возможность явно обязать разработчиков не разбрасывать код в совершенно неожиданных местах.

Исходный код.

Dec 1, 2009

Пустой License Agreement диалог в Setup project

Setup проекты удобны для создания простых инсталляционных пакетов. Они предоставляют довольно широкий спектр различной функциональности. Среди них такие диалоги как лицензионное соглашение (License Agreement dialog) и справку (Read Me). Чтобы их добавить достаточно указать .rft файл с содержимым, которое будет отображаться пользователю.

Но если создать файл в обычном текстовом редакторе (notepad например) и сохранить его как rtf, то при установке пользователь увидит пустое сообщение. В общем чтобы такого не было необходимо rft файлы сохранять через WordPad или что-то подобное.