Nov 30, 2009

Делаем роли явными

Версия 2. Исправления с учетом комментариев.

Данный пост появился в результате многократного прослушивания лекции Udi Dahanan, которая называлась Intentions and Interfaces - Making Patterns Complete (смотреть тут, если не посмотрите, то дальше в общем то читать будет не интересно).

Главная идея лекции заключается в том, чтобы сделать роли в приложении явными. Т.е. если создается система для электронной коммерции, где можно делать заказы, то явно выделить такие интерфейсы (роли) как IMakeOrders, IPayment и т.д. Это позволит четко разделить части приложения, отвечающие за разную функциональность.

Была затронута небезынтересная тема ORM средств, в частности NHibernate, и приведен следующий пример кода:

public class ServiceLayer
{
    public void MakePreferred(Id customerId)
    {
        IMakeCustomerPreferred c = ORM.Get<IMakeCustomerPreferred>(customerId);
        c.MakePreferred();
    }
}

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

Первая попытка реализовать данный подход это использовать схемы наследования, доступные в NHibernate. Не получилось это сделать по причине отсутствия таблицы для интерфейсов (IMakeOrders, IPayment и т.д.), так же нельзя вводить и колонку descriminator, поскольку один объект может реализовывать более одного интерфейса. В общем чистыми маппингами такого дизайна приложения не достичь.

Udi Dahanan запостил у себя на блоге реализацию своего подхода.  Честно сказать – не понравилось. Да, возможно такая реализация как то ближе к DDD или к SOA или еще к куче умных слов, но выделять интерфейс для для каждого бизнес объекта мне кажется излишним.

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

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

Рассмотрим следующую модель:

imageПользователь реализует 2 роли, IOrderMaker и IPreferredMaker. Теперь хотелось бы иметь возможность написать на aspx странице следующий код:

private void MakeOrder(int productId, int customerId, int amount)
{
    Repository<Product> productRepository = new Repository<Product>(Session);
    Repository<Customer> customerRepository = new Repository<Customer>(Session);

    Product product = productRepository.Get(productId);

    IOrderMaker orderMaker = customerRepository.Get<IOrderMaker>(customerId);
    orderMaker.MakeOrder(product, amount);
}

Чтобы это сделать был реализован метод Get в классе Repository:

public TRole Get<TRole>(int id) where TRole:IRole
{
    return (TRole) (Object) session.Get<TEntity>(id);
}

Но в данном случае никто не мешает использовать напрямую поля и методы класса Customer, которые он реализует через интерфейс IOrderMaker, чтобы скрыть их можно воспользоваться интерфейсами с явной реализацией. Тогда метод MakeOrder будет виден только при приведении типа Customer к IOrderMaker. Если Вам это не нужно, можно оставить их простыми методами.

Теперь обратимся к роли IPreferredMaker. Допустим пользователь может стать избранным, только если он сделал более 5 заказов. В таком случае следует заставить репозиторий получить покупателя со всеми его заказами. Это делается при помощи fetching strategy. Изменять её хотелось бы независимо от реализации MakePreferred. Поэтому был задан следующий интерфейс:

/// <summary>
/// Sets fetching strategy for specified role
/// </summary>
/// <typeparam name="TEntity"><see cref="Entity"/></typeparam>
/// <typeparam name="TRole"><see cref="IRole"/></typeparam>
public interface IFetchingStrategy<TEntity, TRole>
{
    void AddFetchTo(ICriteria criteria);
}
И реализация:
/// <summary>
/// Loads customer with his orders
/// </summary>
public class MakeCustomerPreferred: IFetchingStrategy<Customer, IPreferredMaker>
{
    public void AddFetchTo(ICriteria criteria)
    {
        criteria.SetFetchMode<Customer>(x => x.Orders, FetchMode.Join);
    }
}
Теперь надо заставить репозиторий использовать эту стратегию, сделать это можно следующим образом (изменяем метод Get):
public TRole Get<TRole>(int id) where TRole:IRole
{
    ICriteria criteria = session.CreateCriteria(typeof(TEntity));
    criteria.Add<TEntity>(x => x.Id == id);

    IEnumerable<Type> fetchingStrategies = GetFetchingStrategies<TRole>();

    foreach (Type fetchingStrategyType in fetchingStrategies)
    {
        IFetchingStrategy<TEntity, TRole> fetchingStrategy = (IFetchingStrategy<TEntity, TRole>)Activator.CreateInstance(fetchingStrategyType);
        fetchingStrategy.AddFetchTo(criteria);
    }

    TRole result = (TRole) criteria.UniqueResult();
    return result;
}

private static IEnumerable<Type> GetFetchingStrategies<TRole>()
{
    return from t in Assembly.GetAssembly(typeof (IFetchingStrategy<TEntity, IRole>)).GetTypes()
           where t.GetInterfaces().Contains(typeof (IFetchingStrategy<TEntity, TRole>))
           select t;
}

Метод GetFetchingStrategies получает все типы, реализующие IFetchingStrategy<TEntity, IRole> (все стратегии для передаваемой роли и сущности). Метод Get создает экземпляр каждой из стратегий по очереди и применяет к запросу. Естественно метод поиска стратегий можно упростить если использовать Service Locator.

Последнее что хотелось бы упомянуть, это расположение файлов в проекте, я обычно делаю примерно так:

Содержимое проекта

О том как тестировать приложения с таким дизайном я напишу позже.

Пример кода

Nov 26, 2009

Uninstall в setup project

Для простых инсталляторов в вижуал студия предоставляет в общем то неплохие setup projects. Раз есть процесс установки, то хотелось бы иметь и процесс удаления, но удалить приложение можно использовав тот же файл msi, который использовался для установки. Поэтому некоторые добавляют msi как составную часть инсталла, и копируют инсталлятор вместе с приложением.

Но windows хранит собственный кеш msi файлов для каждого установленного приложения. А значит хотелось бы воспользоваться им, а не тянуть инсталляху в корне приложения. Сделать это можно следующим образом:

  1. На сетап проекте клацаем правой кнопкой мыши, там View->FileSystem, выбираем Application Folder. Это список файлов, которые будут скопированы при установке на пользовательскую машину.
  2. Нажимаем правой кнопкой и выбираем Add->File и добавляем bat файл следующего содержания (я думаю как создать .bat файл объяснять не надо) :
    %windir%\system32\MsiExec.exe /I{Id вашего приложения}
  3. Чтобы увидеть Id вашего приложения правый клик на сетап проекте и выбираем свойства. Нас интересует ProductCode. Это Guid, который надо скопировать, и вставить в bat файл на месте “Id вашего приложения”. Обратите внимание, что {} должны остаться.

Ну чтобы он не задавал лишних вопросов можно поискать другие флаги для утилиты MsiExec.exe.

Nov 24, 2009

Локализация веб приложений

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

  1. Если использовать локальные ресурсы, то получение строки из них будет следующим: this.GetLocalResourceObject(“Key”); С этой записью есть следующие проблемы:

    • Этот метод возвращает Object, соответственно чтобы получить строку из этой и без того громоздкой записи необходимо написать GetLocalResourceObject(“Key”).ToString();
    • Поскольку метод возвращает Object, то при отсутствии соответствующего ключа в файлах ресурсов он возвращает Null. И узнаете Вы об этом Null только в момент выполнения (и то если вызовете ToString(), чаще же используется запись <%= GetLocalResourceObject(“Key”) %> которая просто ничего не выводит).

    Причиной этих проблем можно назвать отсутствие автоматической генерации кода. Для папки AppGlobalResources у каждого resx файла есть так же Designer.cs файл, в котом создается класс предоставляющий доступ к каждой строке. Этот класс решает проблемы отсутствующих ключей и обеспечивает intellysence решая в общем то все указанные проблемы. 

  2. Использование глобальных ресурсов. Глобальные ресурсы лишены недостатков локальных, но есть одна проблемка: невозможность разложить ресурсы по папкам и пространствам имен. Чем больше проект, тем больше строк в ресурсах, тем больше файлов. Со временем папка AppGlobalResources превращается в огромный склад, в котором довольно сложно что-либо найти. При этом генератор кода раскладывает классы вне зависимости от папок, в которых они лежат, просто в пространство имен Resources.

  3. И последняя, хотя пожалуй наиболее важная проблема. Если необходимо получить локализованную строку не в веб части проекта. Например:

    Наследование с названием типа объекта

    Есть абстрактный класс Role с абстрактным свойством DisplayName. Это очень удобно когда в интерфейсе необходимо показывать какие роли привязаны к пользователю. Обычно такие классы будут находится в модуле бизнес логики, которая не может содержать ссылки на веб приложение.

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

Но в реализации по умолчанию есть одна важная проблема – классы помечены как internal. Можно конечно изменить код вручную, но при сохранении файла .resx генератор будет запущен заново, и отменит все изменения сделанные вручную.

Я собирался написать шаблоны с использованием t4Toolbox, но поискав чуть чуть, оказалось что велосипед уже изобретен. В данной статье все довольно четко расписано, и проблем с использованием быть не должно. Данный генератор раскладывает генерируемые файлы по пространствам имен согласно структуре папок и сгенерированные классы помечены как public, а так же другие полезные фичи.

Nov 23, 2009

Continuous integration server – Cruise control .net.

Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible.

Это определение находится в статье Мартина Фаулера.

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

Мой первый опыт автоматизации процесса билда проекта был с использованием MsBuild. Я написал скрипт, выполняющий нужные мне действия в нужной последовательности. Файлик находился под управлением контроля версий и все было хорошо. До тех пор, пока не понадобилось обновить собственно сам скрипт. MsBuild загружает файл один раз и потом выполняет все действия указанные в нем, значит обновленную версию скрипта можно получить только после запуска устаревшего. Как результат, для получение последней версии продукта необходимо дважды выполнять процесс билда (пжервый раз со старым скриптом, который заставит обновить исходный код, второй с новой версией, который выполнит необходимые действия). Это одна из причин почему я считаю что один билд скрипт не достаточно удобен.

В этом посте будет рассмотрено как быстро настроить CI (Continuous Integration) сервер используя Cruise control .net.

По теме настройки сервера так же можно почитать следующие два туториала:

Итак начнем.

  • Установка
  • Добавляем проект
  • Настройка системы контроля версий
  • Билд
  • Тестирование
  • Уведомления

Установка

Скачать CC.NET можно тут. Для этого поста используется последняя на данный момент стабильная версия, а именно 1.5.

Во время установки CC.NET создаст виртуальный каталог в IIS, по умолчанию имя этого каталога ccnet. Таким образом попасть на страницу билд сервера можно будет попасть по адресу http://localhost/ccnet.

В папке, куда будет уставлен CC.NET будет создано 3 папки Examples, server и webdashboard.

В папке server нас будет интересовать файл ccnet.config – это обычный xml файл, в котором  хранятся настройки для проектов. Т.е. в нем описывается последовательность действий (таких как получить изменения из репозитория кода, сбилдить проект, запустить тесты и т.д.). Документацию по настройкам в этом файле можно найти тут.

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

Добавляем проект

Чтобы добавить проект добавляем в файл ccnet.config тег project, чтобы получилось примерно следующее:

<cruisecontrol xmlns:cb="urn:ccnet.config.builder">
	<project> 
	<name>Имя проекта</name> 
	<workingDirectory>Рабочая директория</workingDirectory> 
	<artifactDirectory>Артефакты</artifactDirectory> 
	<webURL>Url для веб</webURL> 
	<project>  
	<tasks> 
		Задания 
	</tasks> 
	<publishers> 
		Сообщение о результатах 
	</publishers>  
</cruisecontrol>
  • имя проекта будет отображаться на сайте, в отчетах и т.д.
  • рабочая директория – место где будут храниться временные файлы
  • артефакты – сюда попадают результирующие файлы логов и отчеты
  • Url для веб – этот адрес используется в письме cc.net, которое отсылается после билда.
  • задания – раздел где указывается какую последовательность действий требуется выполнить в процессе билда
  • сообщение о результатах – как оповещать команду о результате билда. Одной из ошибок бывает расположение рассылки о результатах в блоке tasks. Но при провале одной из последовательных задач оставшаяся очередь не выполняется, и значит никто не получит уведомления. Блок publishers не может стать причиной неудачного билда, только tasks

Настройка системы контроля версий

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

Сам раздел описывать не вижу смысла, он слишком очевиден и очень подробно описан, но есть одна особенность работы с svn – CC.NET использует команды для работы с svn вида “svn commit”. После чего говорит что не находит нужную программу для выполнения. Это происходит потому, что устанавливая turtoise svn он не содержит файла svn.exe, наличие которого предполагает CC.NET. Заставить работать удалось только при помощи установки visual svn, в котом есть файл svn.exe.

Билд

Для автоматизации билда можно использовать либо MsBuild, либо NAnt. У меня есть опыт использования обоих.

Скрипты MsBuild для меня показались очень большими и неудобными. Он генерируются вижуал студией в файлах csproj. И все хорошо пока не приходится писать эти вещи вручную. Далее становится все неудобно и слишком длинные названия начинают напрягать и раздражать. Но это лично мои впечатления. Еще одна проблема в том, что MsBuild никак не сможет сбилдить Setup project.

Теперь про NAnt. Честности ради стоит заметить что и NAnt не умеет билдить Setup project’ы, но его можно настроить использовать вижуал студию в качестве средства билда. Xml файлы для NAnt очень похожи на MsBuild и соответственно знание последнего обеспечит легкий переход, но в то же время они меньше, теги называются адекватней (опять же исключительно мое субъективное мнение).

Итак создадим первое задание – компиляция проекта. Для работы с NAnt (так же как и с MsBuild) СС.NET содержит специальный таск. Изменяем ccnet.config чтобы получилось следующее:

<tasks>
	<nant> 
		<executable>C:\nant\nant-0.86-beta1\bin\nant.exe</executable> 
		<baseDirectory>C:\builds</baseDirectory> 
		<buildFile>default.build.xml</buildFile> 
	</nant>
</tasks>

Указываем путь к nant, указываем директорию в которой лежит скрипт, указываем название самого скрипта. Содержимое default.build.xml может быть следующим:

<project default="default"> 

<property name="devEnvPath" value="C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" /> <target name="compileDebug"> <exec program="${devEnvPath}"> <arg value="Путь к .sln файлу" /> <arg value="/build"/> <arg value="Debug" /> </exec> </target> <target name="default" depends="compileDebug" /> </project>

Не знаю что тут можно прокомментировать, вроде все очевидно.

Тестирование

Запуск тестов

Для тестов с использованием NUnit CC.NET содержит готовый таск. Самый простой вариант:

<nunit> 
	<path>C:\Program Files\NUnit 2.5.2\bin\net-2.0\nunit-console.exe</path> 
	<assemblies> 
		<assembly>Путь к сборке с тестами</assembly> 
	</assemblies> 
</nunit>

Если хотя бы один тест провалится, весь таск будет считаться не выполненным. На этом закончится выполнение тасков, и CC.NET начнет выполнение паблишеров, которые должны сообщить о результате билда.

Проверка покрытия

Еще одной очень полезной вещью может быть задание на проверку покрытия кода тестами. Для того, чтобы получить эту информацию можно воспользоваться утилитой NCover. Она строит очень подробные и информативные репорты. Для интеграции в CC.NET можно использовать задание NCover Profiler. Этот таск запустит тесты и сохранит отчет о покрытии тестами. Например таск может быть таким:

<ncoverProfile> 
	<executable>C:\Program Files\NCover\NCover.Console.exe</executable> 
		<program>C:\Program Files\NUnit 2.5.2\bin\net-2.0\nunit-console.exe</program> 
		<testProject>Путь к dll с тестами</testProject> 
		<includedAssemblies>*.dll</includedAssemblies> 
	</ncoverProfile>

Это задание проверит покрытие всех dll вашего проекта.

Так же есть полезный таск NCover Reporting. C помощью него на основании результатов полученных после NCover Profiler можно генерировать html отчеты.

<ncoverReport> 
  	<executable>C:\Program Files\NCover\NCover.Reporting.exe</executable> 
	<outputDir>Reports</outputDir> 
</ncoverReport>

Чтобы увидеть результаты работы этого таска в веб интерфейсе необходимо отредактировать файл dashboard.config (находится в папке webdashboard). В разделе buildPlugins необходимо добавить следующее:

<htmlReportPlugin description="Full NCover Report" actionName="FullNCoverBuildReport" htmlFileName="NCover\fullcoveragereport.html" />

После этого в отчете по билду слева появится новая ссылка:

Full NCover Report link

Уведомления

За уведомления отвечает раздел конфигурации publishers. CC.NET содержит множество готовых способов уведомления (email, rss, cc.net tray и т.д.). Я использую email, конфигурация довольно проста:

<email from="адрес с которого посылать письмо" mailhost="smtp сервер" mailport="25" includeDetails="true" useSSL="false" mailhostUsername="имя пользователя" mailhostPassword="пароль">
	<users>
		<user name="имя пользователя" group="группа" address="адрес"/>
	</users>
	<groups>
		<group name="buildmaster" >
			<notifications>
				<notificationType>Always</notificationType>
			</notifications>
		</group>
	</groups>
	<xslFiles>
		<file>xsl\header.xsl</file>
		<file>xsl\compile.xsl</file>
		<file>xsl\unittests.xsl</file>
		<file>xsl\modifications.xsl</file>
	</xslFiles>
</email>

Результаты каждого билда будут высылаться согласно этой части конфигурационного файла.

Nov 11, 2009

aspnet_regiis на x64 windows

В своих попытках разобраться с CruiseControl.NET поставил чистую виртуалку с windows 2008 x64. После чего поставил IIS и CruiseControl.

По умолчанию IIS не регистрирует asp.net. Естественно все знают что для этого стоит выполнить утилиту aspnet_regiis с флагом –i. Но меня крайне смутило, что утилита начала выдавать ошибку типа приложение обратилось к памяти по адресу … и не может прочесть и т.д.

Поискав решение оказалось что для x64 платформ в папке Microsoft.NET есть не только папка Framework но и Framework64. Посмотрев в неё я обнаружил что regiis в этой папке работает без ошибок.

Итак для x64 платформ для регистрации asp.net в IIS выполнять нужно aspnet_regiis.exe –i находящийся в папке C:\Windows\Microsoft.NET\Framework64\v2.0.50727.

Возможно это кому то сохранит лишние 10 минут времени

Nov 6, 2009

Inversion of control. Управление зависимостями.

Данный пост является некоторым следствием работы над последним проектом. В этом проекте использовалась S#arp архитектура, которая обеспечивает отличную возможность для юнит тестирования и каркас для создания корпоративных веб приложений. Так же крайне полезно будет прочесть статью касающуюся зависимостей. Я считаю что разобраться в чем то лучше всего позволяет попытка объяснить это другому. Именно это и я и хочу сделать.

Рассмотрим пример программы. Пусть приложению необходима некоторая система ведения логов. Логи сохраняются в файл для дальнейшего анализа. Для подобных требований вполне логично было бы использовать следующий код:

public class Logger 
{ 
 public void LogMessage(string message) 
 { 
  using(FileStream fileStream = new FileStream("log.txt", FileMode.Append, FileAccess.Write)) 
  { 
   StreamWriter writer = new StreamWriter(fileStream); writer.Write(message); 
    }
 } 
} 

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

Получив такое требование конечно хотелось бы добавить новый метод LogMessage(string message, bool storeInDataBase), который принимает дополнительный параметр типа bool и если он true пишет все что надо в базу.

Проблема в этом подходе я думаю уже видна – если в дальнейшем мы захотим писать не только базу данных и в файл, но в Windowns EventLog, то что, добавлять еще один флаг?

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

image

Наш класс Logger напрямую зависит от файловой системы. Чтобы избавится от этой зависимости можно использовать принцип Inversion of control. Для этого выделим интерфейс, который будет отвечать за сохранение информации в лог:

public interface ILogContainer 
{ 
 void SaveMessage(string message); 
} 

И каждый класс, который который будет отвечать за сохранение логов пусть реализует данный интерфейс. Например для тех же файлов это может быть такой класс:

public class FileLogContainer: ILogContainer 
{ 
 public void SaveMessage(string message) 
 { 
  using (FileStream fileStream = new FileStream("log.txt", FileMode.Append, FileAccess.Write)) 
  { 
   StreamWriter writer = new StreamWriter(fileStream); writer.Write(message); 
  } 
 } 
} 

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

public class Logger 
{ 
 private ILogContainer logContainer; 
 public Logger(ILogContainer logContainer) 
 { 
  this.logContainer = logContainer; 
 } 
 public void LogMessage(string message) 
 { 
  logContainer. SaveMessage(message); 
 } 
}

После этого преобразования диаграмма зависимостей будет выглядеть:

image

Из диаграммы понятно почему этот подход называется Inversion of control, получается что зависимость от файловой системы была как бы развернута в обратную сторону.

Что нам дает этот подход? Помимо того, что теперь мы можем использовать любое хранилище логов, реализующее ILogContainer, полученный результат отлично поддается тестированию. Интерфейс ILogContainer дает точку в которй можно встроить mock object, с помощью которого можно правильно проверить работу класса Logger.

Nov 5, 2009

Fluent mapping conventions. Автомаппинг.

Линки на остальные статьи

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

image

И храним все это в следующей базе:

image

По прежнему имеется категория, продукт и заказ. Но у продукта несколько больше свойств нежели в предидущем примере. Для маппинга этого объекта пришлось бы указывать каждое свойство по очереди вызывая метод Map.

Но это скучно и не понятно зачем, ведь имена этих свойств и так можно получить при помощи рефлексии. Так же существует четкий принцип в программировании - DRY (don’t repeat yourself). Я думаю никому не надо объяснять почему повторяющийся код это плохо. А мы при маппинге очень часто повторяем себя поскольку вероятно имена полей будут совпадать с именами колонок в таблицах и т.д. Итак чтобы этого избежать в Fluent NHibernate существует Automapping.

Чтобы воспользоваться им создадим следующую структуру файлов:

image

Собственно исходный код классов в папке (и соответственно неймспейсе) Entities можно посмотреть в примере о связях.

Интерес представляет папка mappings. Начнем по порядку. Для генерирования маппингов Fluent NHibernate использует Conventions (соглашения). Они устанавливают правила формирования названий полей объектов, названий таблиц и т.д.

CategoryMap, OrderMap, ProductMap – к ним мы обратимся позже, это классы, которые позволяют дополнять или же переопределять маппинги полученные при помощи conventions.

ModelGenerator – класс, которой собственно и соберет все воедино. Итак рассмотрим его исходный код:

public class ModelGenerator
{
 public AutoPersistenceModel Generate()
 {
  AutoPersistenceModel automap = new AutoPersistenceModel();

  automap.Conventions.AddFromAssemblyOf<ModelGenerator>();
  automap.UseOverridesFromAssemblyOf<ModelGenerator>();

automap.AddEntityAssembly(Assembly.GetAssembly(typeof (Product))) .Where(x=>x.Namespace.EndsWith("Entities")); return automap; } }
  • 5 строка – добавить все conventions из сборки, которая содержит класс ModelGenerator
  • 6 строка – взять все переопределения из сборки содержащей ModelGenerator (далее подробней)
  • 7 строка – объекты, которые необходимо замапить находятся в сборке, содержащей Product
  • 8 строка – среди объектов взять только те, неймспейс которых заканчивается на Entities

Для примера я создал 2 соглашения. 1 для идентификаторов:

public class PrimaryKeyConvention : IIdConvention
{
 public void Apply(IIdentityInstance instance)
 {
  instance.GeneratedBy.Native();
  instance.Column("Id");
 }
}

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

Соглашение для имен таблиц:

public class TableNameConvention : IClassConvention
{
 public void Apply(IClassInstance instance)
 {
  instance.Table(Inflector.Net.Inflector.Pluralize(instance.EntityType.Name)); 
 }
}

Тут используется Inflector, для того, чтобы в базе данных имена таблиц были в множественном числе. Например таблица с продуктами – Products, с категориями – Categories, в то время как объекты называются Product и Category.

Вообще говоря, если создавать базу с нуля, и следовать простым правилам, то все связи можно создавать при помощи соглашений, вообще не приходя к ручному маппингу. Но естественно не всегда есть такая возможность либо какой то из маппингов нужно изменить. Допустим по умолчанию (по соглашениям) для коллекций установлен lazy load. А с точки зрения производительности для определенного объекта его надо выключить. Это можно сделать при помощи переопределений маппингов. Именно для для этого в классе ModelGenerator указано UseOverridesFromAssemblyOf(...). Рассмотрим как это работает:

public class CategoryMap : IAutoMappingOverride<Category>
{
 public void Override(AutoMapping<Category> mapping)
 {
  mapping.HasManyToMany(x => x.Products)
   .Access.CamelCaseField()
   .Cascade.SaveUpdate()
   .AsSet()
   .ParentKeyColumn("CategoryId")
   .ChildKeyColumn("ProductId")
   .Table("Products_Categories");
 }
}

public class OrderMap : IAutoMappingOverride<Order>
{
 public void Override(AutoMapping<Order> mapping)
 {
  mapping.References(x => x.Product, "ProductId");
 }
}

public class ProductMap : IAutoMappingOverride<Product>
{
 public void Override(AutoMapping<Product> mapping)
 {
  mapping.HasManyToMany(x => x.Categories)
    .Access.CamelCaseField()
    .Table("Products_Categories")
    .ParentKeyColumn("CategoryId")
    .ChildKeyColumn("ProductId")
    .AsSet()
    .Inverse()
    .Cascade.SaveUpdate();

  mapping.HasMany(x => x.Orders)
    .KeyColumn("ProductId")
    .Inverse()
    .Cascade.AllDeleteOrphan();
 }
}

Как можно увидеть перепределение ничем не отличается от обычного Fluent маппинга с одним лишь различием – не надо упоминать каждое свойство.

Ну и в конце собственно изменения в файле Global.asax:

protected static ISessionFactory CreateSessionFactory()
{
 ModelGenerator modelGenerator = new ModelGenerator();
 var sessionFactory = Fluently.Configure()
    .Database(MsSqlConfiguration.MsSql2005.ConnectionString(@"connection string"))
    .Mappings(m =>m.AutoMappings.Add(modelGenerator.Generate()))
    .BuildSessionFactory();
 return sessionFactory;
}

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

Nov 4, 2009

NHibernate для начинающих. Часть 4 Criteria queries

Линки на остальные статьи

Основным средством построения запросов в NHibernate является Criteria api (до тех пор, пока не реализуют полноценный Linq to Hibernate). Поэтому я опишу некоторые примеры запросов, которые можно строить с их использованием. Для этого воспользуемся моделью, описанной тут.

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

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

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));

const string requiredSubstring = "prod";
criteria.Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere));
criteria.SetMaxResults(10);
criteria.SetFirstResult(0);
criteria.AddOrder(NHibernate.Criterion.Order.Asc("Description"));

IList<Product> result = criteria.List<Product>();

Итак что здесь происходит:

  • 1 строка очевидно – создаем сам критерий.
  • 4 строка – в критерий добавляется условие, которое говорит о том, что объекты, которые войдут в результирующий рекорд сет в свойстве Description должны содержать requiredSubstring. Последний параметр говорит о том, где именно должна быть эта подстрока. Можно создать критерий, который говорит что название должно начинаться с указанного слова или заканчиваться на него. Прелесть этого запроса в том, что мы оперируем исключительно доменными объектами, мы вообще не заботимся о том, как NHibernate построит необходимый запрос и какие поля/таблицы затронет.
  • 5 строка – указываем, что результатов должно быть не больше 10.
  • 6 строка – выдавать результаты начиная с 0го. Т.е. например, у нас в базе есть 20 продуктов, из них 15 в названии содержит «prod». Значит данный запрос выведет 10 первых продуктов.
  • 7 строка – сортировка.

Далее рассмотрим следующий пример:

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

const string requiredSubstring = "prod";

const double requiredPrice = 5.0;
criteria.Add(new Conjunction()
  .Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere)) 
  .Add(Restrictions.Gt("Price", requiredPrice))
      );

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

criteria.Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere) &&
    Restrictions.Gt("Price", requiredPrice)
             );

Как Вы видите между двумя условиями стоит обычный оператор &&. Так же можно использовать и || где это необходимо (например если бы надо было найти товар у которого подстрока содержится либо в названии либо в описании).

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

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));
const string requiredSubstring = "prod";
criteria.Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere));
criteria.SetProjection(Projections.RowCount());
int result = criteria.UniqueResult<int>();

Теперь посмотрим на следующий запрос. Найти все продукты, цена которых выше стредней. Для этого необходимо воспользоваться подзапросом, который будет вычислять среднюю цену продуктов. Итак:

DetachedCriteria avgCritegia = DetachedCriteria.For<Product>();
avgCritegia.SetProjection(Projections.Avg("Price"));

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));
criteria.Add(Subqueries.PropertyGt("Price", avgCritegia));

IList<Product> result = criteria.List<Product>();

NHibernate выполнит следующий запрос:

SELECT this_.Id as Id0_0_, this_.Description as Descript2_0_0_, this_.Name as Name0_0_, this_.Price as Price0_0_
FROM Products this_ 
WHERE this_.Price > (SELECT avg(cast(this_0_.Price as DOUBLE PRECISION)) as y0_ FROM Products this_0_)

Теперь посмотрим на запрос к объектам с коллекциями. Допустим надо найти все продукты, которые заказывал покупатель с определенной строкой в имени:

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));

const string requiredCustomerName = "cust";
criteria.CreateCriteria("Orders")
 .Add(Restrictions.Like("Customer", requiredCustomerName, MatchMode.Anywhere));

IList<Product> result = criteria.List<Product>();

NHibnerate выполнит следующий запрос:

SELECT this_.Id as Id0_1_,
 this_.Description as Descript2_0_1_,
 this_.Name as Name0_1_,
 this_.Price as Price0_1_,
 order1_.Id as Id3_0_,
 order1_.NumberOfItems as NumberOf2_3_0_,
 order1_.Customer as Customer3_0_,
 order1_.ProductId as ProductId3_0_
FROM Products this_ inner join Orders order1_ on this_.Id=order1_.ProductId
WHERE order1_.Customer like @p0
@p0=N'%cust%'

Естественно что в .net 3.5 уже очень не хотелось бы использовать названия свойств в виде строк. Nз-за этого приходится постоянно лазить в исходник самого объекта и смотреть там точное название необходимого ствойства. Чтобы избежать этого существует QueryOver API. Они позволяют переписать все выше упомянутые запросы, но в строго типизированном виде.