Nov 29, 2010

TFS for the SVN user

Originally I wanted to name this post as Making your life a little bit easier with all TFS horror and then decided to change it. The reason for changing title is that my irritation on TFS was called only by VisualSVN usability. I spoke with people who are using TFS for long period and have never used any other tools and they say that it is really good and everything is just obvious. Obvious for them, but not for me.

Currently I have no choice except using TFS as a source control system.  Previously I used Subversion with VisualSVN add-in to integrate with visual studio.

The only way for me not to go crazy while doing really simple tasks with TFS (like commit, merging) was making some configurations. Maybe some tips will save someone's time. So lets start with them one by one.

Merging tool

First of all when you merge conflicts with build in merging tool you really won’t understand what has changed. Changes are shown only in rows. So if one symbol is change you will need to hunt it in entire row by yourself. Also it is hard for me to see where conflict was resolved successfully with no need for me to do something and where my assistance is required.

Now I’m using WinMerge tool. Its also not the best, but really better then default one. To configure it you need to go to the Tools->Options->Source Control->Visual Studio Team Foundation Server and click Configure User Tools button. Click Add… button to add new command.

User tool configuration window

Select Compare operation, provide to WinMerge and fill arguments with this value: /ub /dl %6 /dr %7 %1 %2.

Click ok and add another new command select Merge, provide WinMerge path and fill arguments: /ub /dl %6 /dr %7 %1 %2 %4. Click Ok.

Here is a post where described how to configure TFS with other tools.

Compare on double click

Another really annoying thing is that when you are about to commit all your changes and you double click on a file it is just opened (in Notepad as I rememer O_O). Why would anyone need to see his pending change file in notepad? To compare it with server version you need to click on special buttons:

Checkin pending changes dialog

I didn’t want to pixel hunt that buttons. I wanted to compare with latest version on double mouse click. The good news that it is possible with TFS. How to configure it you can read here (btw the only option that worked for me is with registry and only after reboot).

Ghost changes

My favourite trick with TFS is just go to WebForm1.aspx press space somewhere and hit ctrl+z. You even didn’t hit save. Then you can open pending changes dialog and there real surprise is waiting for you. TFS will say that you have 3 pending changes Open-mouthed smile. Wow, when I saw this for the first time I couldn’t believe my eyes!

Ofcourse  when you click Check In button it will check real changes in the file and won’t commit file with empty changes. But I used to look through all changes that I’m going to commit and don’t want to hunt for really changed files among rest of them.

To solve this TFS power tools can be used. They contain a lot of useful features and one of them is TFPT.EXE tool. It is command line utility and can do a lot of stuff that you usually do with interface. And guess what? This tool has a special command to roll back unchanged files.

I added this tool as an external tool. To do this you can do the following go to the Tools->External Tools and click Add button. There you need to fill all the fields like this:

external tools configuration

I named the command as undo. You can name it the way you want. After you do it in Tools menu new option will be added:

undo

When clicked following output will be shown for me:

undo results

Auto merge

Another reason why you would want to install power tools is auto merge all button.

automerge all

The thing is: when you update, even if TFS is able to merge changes by himself it will force you to hit auto merge, auto merge, auto merge on each conflicted file. With this button you can do it in one place for all files. If somebody knows how to force it auto merge when possible without me clicking something I would really appreciate your help.

Remembering credentials

It won’t be an issue for the most of the TFS users, but it was for me.

Our TFS server is located in a different active directory domain. And each time you open a solution it asks you for the credentials and has no option to save them. I found this question on the msdn. The solution that worked for me is:

  1. Open start menu and in the quick search find for “manage network passwords” (in Russian version of windows you need to look for “управление сетевыми паролями”)
  2. In the opened window click on Add a Windows credential
  3. Fill all the required fields and click ok (server name should be provided with no http or something else, just name)

After doing this Visual Studio won’t ask password.

Unshelve with merge

I didn’t really used this feature of power tools yet. But as I was told default unshelving functionality will just replace all the files in your working copy. And if time has passed since you shelved all changes will be lost. Power tools allows you to merge changes and not just override.

So I think that those are all the configurations that I have made for today. It I remember something else I’ll write a new post.

Nov 11, 2010

Don't use constants

I don’t really remember when, but inside my current team I have told that I really hate to see classes that are like this:

public class Contants
{    
    public class Settings
    {
        public const ConnectionStringName = "SomeProject.ConnectionString";    
    }
    public class FieldNames
    {
        public const SomeField = "MyField";
    }
}

Almost all of the projects that I saw had this kind of class. Usage of it seams to be obvious – to store reusable strings to remove repetition.

But for me such a classes are lacking of the one thing – intention. Each constant exists inside our application for some reason and Constants class completely removes context that is connected to its value.

Today we had a some kind of “real world” scenario where I can show what to do with constants. I our case we had an email template where system will substitute some user input. So template contains %UserName%, %Reason% and so force string patterns that are going to be replaced with some values. The most obvious storage for these patterns is Constants class, but we don’t want it :)! So what I think we should do is to create a set of extension methods like this:

static class StringExtensions
{
    public static string SubstituteUserName(this string template, string userName)
    {
        return template.Replace("%UserName%", userName);
    }
}

This approach will allow us easily to write unit tests, because method is separated from others and really simple, and it will allow to write code in the following way:

"Hello %UserName%, thanks for your message about %Reason%"
                      .SubstituteUserName(userName)
                      .SubstituteReason(reason);

I think its really readable and looks nice.

The next thing why such an approach works is the way how developer works with the legacy code. In legacy system it is really often for developer to perform text based search to find places where similar tasks were already solved. For example when you need to get some value from the web.config file you can either use ConfigurationManager or WebConfigurationManager or maybe in your project some custom wrapper is implemented. For me the easiest way to find such information is to grab existing key of some value and try find all the references on it. In the good project you will meet only one place of usage :). In other cases it may be two places – Constants class and some domain specific wrapper (in this case why do need this constant separately of its usage?). But if you find that this constant is reused in 100 places and there is no common pattern you will just add the 101th place and forget about it. The reason why that happened is the existence of this Constans class.

So my approach for constants is that they can be only private in some classes that are using them. This allows new developers easy way to understand why those values are here and how to use them.

Nov 2, 2010

ConfOrm nice column naming conventions

I have decided to write all further posts in English. The reason for that is simple – when I try to find something related to programming topics I always use English. I think most people do the same.

The next step of learning ConfOrm for me was creation of sharp-architecture like conventions for table and column mappings.  I even wrote some pattern appliers, but when ManyToMany turn has came I had to google a bit. And the result was that Conform.Shop already implemented everything that I wanted so this post will be about usage of this library and the next one about implementation of your own appliers.

To create your own naming conventions we need to understand overall process. Basically mapping in ConfOrm  consists of two major steps:

  1. Getting the domain entities. This is done with the help of ObjectRelationalMapper class. This class is responsible for creating all the entities and associations between them. So is you want to change association type, or remove some property from the mappings this a place where you should do it.
  2. Creating mappings. Mapper class is responsible here. If you want to provide tables, columns and other conventions this is the right place.

Lets begin with domain model:

domain model 

Simple but with some associations.

Probably the easiest way to understand how NHibernate is configured is to look at XML (ConfOrm doesn't generate XML, but you can get it with this approach). So lets check User entity map:

<class name="User">
  <id name="Id" type="Int32">
    <generator class="native" />
  </id>
  <property name="FirstName" />
  <property name="LastName" />
  <property name="VeryLongProperty" />
  <set name="Orders" inverse="true" cascade="all,delete-orphan">
    <key column="User" on-delete="cascade" />
    <one-to-many class="Order" />
  </set>
</class>

The first thing that I want to change is the table name. I want it to be plural. To do this all that is required to do the following:

var englishInflector = new EnglishInflector();
mapper.PatternsAppliers.Merge(
                       new ClassPluralizedTableApplier(englishInflector));

There are also SpanishInflector and ItalianInflector if somebody wants them. After adding this one we get directive table=Users and so on for all other entities.

Next thing that I don’t like is names for column in the <key property. To change it I need to do the following:

mapper.PatternsAppliers.Merge(
new OneToManyKeyColumnApplier(relationalMapper));

Now the mapping for the set is:

<set name="Orders" inverse="true" cascade="all,delete-orphan">
  <key column="UserId" on-delete="cascade" /> 
  <one-to-many class="Order" /> 
</set>

That is probably all that I wanted to change in User mapping. Now Lets look at the Product map:

<class name="Product" table="Products">
  <id name="Id" type="Int32">
    <generator class="native" />
  </id>
  <property name="Price" />
  <property name="Name" />
  <property name="Description" />
  <set name="Categories" table="CategoryProduct" inverse="true">
    <key column="product_key" />
    <many-to-many class="Category" column="category_key" />
  </set>
</class>

There is definitely a better way of naming columns in the joining table. To change them we need to add next appliers:

mapper.PatternsAppliers.Merge(
                      new ManyToManyColumnApplier(relationalMapper));
mapper.PatternsAppliers.Merge(
                      new ManyToManyKeyIdColumnApplier(relationalMapper));

Those will give:

<set name="Categories" table="CategoryProduct" inverse="true">
  <key column="ProductId" />
  <many-to-many class="Category" column="CategoryId" />
</set>

That is what I really wanted. So all the mappings for the domain:

var relationalMapper = new ObjectRelationalMapper();
relationalMapper.TablePerConcreteClass(domainEntities);
relationalMapper.Patterns.PoidStrategies.Add(new NativePoidPattern());
relationalMapper.Cascade<Category, Product>(Cascade.Persist);
relationalMapper.ManyToMany<Category, Product>();
relationalMapper.Cascade<Order, Product>(Cascade.Persist);

var mapper = new Mapper(relationalMapper);
var englishInflector = new EnglishInflector();
mapper.PatternsAppliers.Merge(new ClassPluralizedTableApplier(englishInflector));
mapper.PatternsAppliers.Merge(new OneToManyKeyColumnApplier(relationalMapper));
mapper.PatternsAppliers.Merge(new ManyToManyColumnApplier(relationalMapper));
mapper.PatternsAppliers.Merge(new ManyToManyKeyIdColumnApplier(relationalMapper));

So not a lot of code and settings. Also this assembly contains a lot of other very nice patterns for example ManyToManyPluralizedTableApplier after applying it table name for joining products and categories becomes  CategoriesToProducts. Just beautiful Smile.

Here is the source code for this post

So use ConfOrm!

Oct 29, 2010

Что нового в NHibernate 3

9 октября был выпущен NHibernate 3 beta 1. Решил посмотреть что в нем нового. Для этого быстренько набросал модель:

2010-10-29_1826

Для маппинга я буду использовать ConfOrm. Его настройки:

public class DomainMapper
{
    public HbmMapping GenerateMappigs()
    {
        IEnumerable<Type> domainEntities = this.GetDomainEntities();

        var relationalMapper = new ObjectRelationalMapper();
        relationalMapper.TablePerConcreteClass(domainEntities);
        relationalMapper.Patterns.PoidStrategies.Add(new NativePoidPattern());
        relationalMapper.Cascade<Category, Product>(Cascade.Persist);
        relationalMapper.ManyToMany<Category, Product>();
        relationalMapper.Cascade<Order, Product>(Cascade.Persist);

        var mapper = new Mapper(relationalMapper);
        mapper.PatternsAppliers.RootClass.Add(new TableNamesApplier());
        mapper.Class<User>(x => x.Property(y => y.VeryLongProperty, map => map.Lazy(true)));
        HbmMapping mapping = mapper.CompileMappingFor(domainEntities);
        return mapping;
    }

    /// <summary>
    /// Gets all objects that are inherited from <see cref="BaseEntity"/>.
    /// </summary>
    private IEnumerable<Type> GetDomainEntities()
    {
       Assembly domainAssembly = typeof(BaseEntity).Assembly;
       IEnumerable<Type> domainEntities = from t in domainAssembly.GetTypes()
                                          where t.BaseType == typeof(BaseEntity) && !t.IsGenericType
                                          select t;
            return domainEntities;
    }
}

В предыдущих постах говорили что стоит добавлять и схему базы данных. Скажу честно, я её не создавал, а попросил это сделать NHibernate. Поэтому я не буду её тут приводить Smile

Чтобы сгенерировать базу данных можно использовать SchemaExport:

new SchemaExport(cfg).Execute(true, true, false);

Дальше по порядку:

1. И конечно самое ожидаемое – Linq Provider

Чтобы попробовать его я решил попробовать запросы, которе прошлый провайдер (основанный на ICriteria) не мог выполнить.

  1. (from u in session.Query<User>()
    where u.Orders.Count > 5
    select u).Count()
    Выполняет следующий sql:
    select
        cast(count(*) as INT) as col_0_0_
    from
        Users user0_
    where
        (
            select
                cast(count(*) as INT)
            from
                Orders orders1_
            where
                user0_.Id=orders1_.OrderUser
        )>@p0;
    @p0 = 5 
  2. Интересно было так же посмотреть как выбираются анонимные объекты, запрос:
    var firstName = (from u in session.Query<User>()
                     select new { u.FirstName }).FirstOrDefault();
    Выполняет
    select
        TOP (@p0) user0_.FirstName as col_0_0_
    from
        Users user0_;
    @p0 = 1 [Type: Int32 (0)]
    Я думаю комментарии тут не нужны, тем более если еще вспомнить с каким количеством баз данных умеет работать NHibernate и насколько это круто Smile
  3. Но вот такой запрос уже выполнить не получилось:
     var rows = (from u in session.Query<User>()
                 let order = u.Orders.FirstOrDefault()
                 where u.Orders.Count > 5 && order != null && order.Id == 4
                 select u).ToList();

2. Fluent синтаксис для конфигурации SessionFactory

Для этого примера мне потребовалась следующая кофигурация:

DomainMapper mapper = new DomainMapper();
HbmMapping generatedMappigs = mapper.GenerateMappigs();

var cfg = new Configuration();
cfg.SessionFactory()
        .Proxy.Through<ProxyFactoryFactory>()             
        .Integrate
            .Using<MsSql2008Dialect>()
            .AutoQuoteKeywords()
            .Connected
                .By<SqlClientDriver>()
                .ByAppConfing("connectionString")
            .CreateCommands
                .ConvertingExceptionsThrough<SQLStateConverter>();
cfg.SetProperty("show_sql", "true"); // I haven't found how to configure them
cfg.SetProperty("format_sql", "true");
cfg.AddDeserializedMapping(generatedMappigs, "WhatsNew");

чтобы использовать такой синтаксис необходимо подключить неймспейс NHibernate.Cfg.Loquacious. Конечно название они выбрали... Но дословно гугл переводит как "словоохотливый". Может оно и правильно, но я раньше нигде не встречал. Наиболее полный пример всех настроек я смог найти у Fabio Maulo.

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

3. QueryOver синтаксис

Это API позволяет упросить написание ICriteria запросов. Во первых больше нет необходимости использовать строки (раньше это решалось с помощью библиотеки NHLambdaExtensions), во вторых теперь возможны сложные условия внутри одного вызова через &&, ||, в третьих упрощена работа с алиасами (можете посмотреть насколько это было не просто в предыдущей версии), в четвертых работа с проекциями теперь гораздо более читабельна, и в пятых мне наверно стоило сделать из этого абзаца n-тых список c номерами Smile.

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

var query = session.QueryOver<User>()
                    .Select(x => x.FirstName, x => x.LastName)
                    .WhereRestrictionOn(x => x.FirstName).IsLike("first", MatchMode.Anywhere)
                    .WhereRestrictionOn(x => x.LastName).IsLike("last")
                    .List<object[]>()
                    .Select(properties => new
                                                {
                                                    FirstName = properties[0],
                                                    LastName = properties[1]
                                                });

Это выполнит запрос:

SELECT
    this_.FirstName as y0_,
    this_.LastName as y1_
FROM
    Users this_
WHERE
    this_.FirstName like @p0
    and this_.LastName like @p1;
@p0 = '%first%' [Type: String (4000)], 
@p1 = 'last' [Type: String (4000)]

Ну и обращаться к результатам можно как и к обычным анонимным объектам:

foreach (var user in query)
{
    Console.WriteLine(user.FirstName);
}

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

4. Lazy Property

Не знаю почему эта фича была реализована только сейчас, но теперь она работает, так если у сущности есть какое то поле, которое не хотелось бы загружать каждый раз, можно пометить его как Lazy. Но с этой штукой надо быть осторожно, потому как если из базы данных достается список таких объектов, то вероятнее всего весь этот список будет перебираться (иначе зачем его было доставать) это может стать причиной Select N+1.  Ну демонстрация очень простая Smile :

var user = session.Get<User>(1);
string veryLongProperty = user.VeryLongProperty;

И запросы:

/* 1 */
    SELECT
        user0_.Id as Id4_0_,
        user0_.FirstName as FirstName4_0_,
        user0_.LastName as LastName4_0_
    FROM
        Users user0_
    WHERE
        user0_.Id=@p0;
    @p0 = 1 [Type: Int32 (0)]
/* 2 */
    SELECT
        user_.VeryLongProperty as VeryLong4_4_
    FROM
        Users user_
    WHERE
        user_.Id=@p0;
    @p0 = 1 [Type: Int32 (0)]

Примечательно то, что благодаря этой фиче смогли реализовать Lazy Load one-to-one связи которой раньше не было. 

5. Нет зависимости от log4net

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

6. Остальное

В releasenotes еще длиннющий список повакшенных багов и некоторых импрувментов. Но для меня наверно они не так понятны потому как никогда на них не натыкался. Ну либо вот такие фичи:

  • [NH-2309] - Add support for Future() with the new Linq provider
  • [NH-626] - Adding XmlDoc to NH types
  • [NH-2135] - Compatible with Mono
  • [NH-2256] - Add support for user-provided extensions to the Linq provider

В общем прогресс проекта виден. Я боялся что EF задавит хибер за счет майкросовской рекламы, но видимо нет, слишком уж он хорош Smile.

Sep 29, 2010

Почему не asp.net и про собеседования

После довольно продолжительной работы с asp.net mvc пришлось перечитать книгу по asp.net. Казалось бы многие утверждают, что начинать проще с веб форм, а только потом уже переходить на mvc. Но давайте посмотрим на 2 картинки из книги для подготовки к экзамену по asp.net:

simple request

Все просто и понятно. Браузер запросил страничку, сервер её построил и вернул. Теперь посмотрим на другую:

 жизненный цикл страницы

Несколько длинный путь по сравнению с первой картинкой…

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

Мало того, если еще гора правил по использованию этих событий (которые почему то все невероятно любят спрашивать на разного рода тестах и собеседованиях) типа: тему и мастер пейдж можно задать только на этапе PageInit, но на PageInit еще не доступен ViewState, последнее событие, на котором можно изменить ViewState это Prerender – на последующих событиях эти изменения игнорируются и т.д.

confused-man Событийная модель asp.net невероятно сложна в понимании. Особенно весело становится, если рассмотреть порядок вызова событий для страницы, которая содержит MasterPage + ContentPage + UserControl. Это список из 17 ОСНОВНЫХ событий,  без учета всех PreInit, PreRender и прочих.
И это все чтобы создать одну html страницу… 17 событий, хотя случилось всего одно событие – на сервер пришел запрос.

Так же в ASP.NET есть поддержка тем (кстати ни одного приложения с использованием этой фичи не встречал), но опять же, есть ряд правил их применения:

порядок применения тем

Подобные примеры можно приводить до бесконечности. Под конец беглого просмотра книги складывается впечатление что asp.net это огромный набор различных “gotcha” и “WTF!?”.

Сертификационные экзамены сплошь набиты вопросами именно на эти темы. Порядок вызовов, приоритеты применения, файлы настроек, и так до бесконечности.

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

В mvc нет событийной модели, но мне совершенно это не мешало, а скорее наоборот сделало гораздо более понятней происходящее в веб. Стало понятно почему приложения должны следовать правилу post-redirect-get, и что означает окошко браузера типа:

2010-09-29_2340

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

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

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

Не могу сказать что сейчас надо всем срочно бросать asp.net и бежать учить mvc. Просто хотелось бы посоветовать обратить внимание больше на саму концепцию веб приложений нежели на конкретную их реализацию. 

Sep 8, 2010

Про ConfORM

ConfORM это еще один способ маппинга NHibernate сущностей используя код. Главное его отличие от FluentNHibernate в том, что ConfORM вообще не генерирует XML, а работает с открытым  в NHibernate 3 API (более подробно об API можно почитать тут).

Ознакомится с этим фреймворком оказалось довольно не просто. Во первых его название практически не поддается поиску, т.е. вбивая confORM в гугле вы врядли найдете что-то связанное с ним. Во вторых у проекта нет wiki, нет документации, найдено было только следующее:

  1. блог Fabio Maulo – менеджер проекта NHibernate, ConfOrm и еще нескольких OSS проектов.
  2. Блог testdrivendevelopment.wordpress.com.
  3. Страница на google code.
  4. И гугло-группа где обсуждается текущее положение вещей.

Вот в общем то и все что мне удалось накопать за 2-3 часа поисков. Если кто-то найдет еще что-то милости прошу добавляйте в коменты.

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

domain_model

Все классы наследуют EntityBase который содержит единственное свойство Id типа int.

Теперь попробуем замапить эту модель. Для этого ConfORM использует класс ObjectRelationalMapper. Использовать его можно следующим образом:

public HbmMapping GenerateMappigs()
{
    IEnumerable<Type> domainEntities = this.GetDomainEntities();
    
    ObjectRelationalMapper relationalMapper = new ObjectRelationalMapper();
    relationalMapper.TablePerConcreteClass(domainEntities); // каждый не абстрактный объкт будет замаплен на свою таблицу.

    Mapper mapper = new Mapper(relationalMapper);
    HbmMapping mapping = mapper.CompileMappingFor(domainEntities); // создание самих маппингов.

    File.WriteAllText(@"d:\mappings.xml", Serialize(mapping)); // сохраняем маппинги в файл.
    return mapping;
}

/// <summary>
/// Gets all objects that are inherited from EntityBase.
/// </summary>
private IEnumerable<Type> GetDomainEntities()
{
    Assembly domainAssembly = typeof (EntityBase).Assembly;
    IEnumerable<Type> domainEntities = from t in domainAssembly.GetTypes()
                                       where t.BaseType == typeof(EntityBase) && !t.IsGenericType
                                       select t;
    return domainEntities;
}

Наверно что-то замапилось :). Чтобы это проверить можно сохранить конфигурацию в XML виде. Для того используется метод Serialize, его реализация такова:

/// <summary>
/// Generates XML string from NHibernate mappings
/// </summary>
protected static string Serialize(HbmMapping hbmElement)
{
    var setting = new XmlWriterSettings { Indent = true };
    var serializer = new XmlSerializer(typeof(HbmMapping));
    using (var memStream = new MemoryStream())
    {
        using (var xmlWriter = XmlWriter.Create(memStream, setting))
        {
            serializer.Serialize(xmlWriter, hbmElement);
            memStream.Flush();
            byte[] streamContents = memStream.ToArray();

            string result = Encoding.UTF8.GetString(streamContents);
            return result;
        }
    }
}

Выполнив этот код, получим следующий xml:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" namespace="Conf.Entities" assembly="Conf" xmlns="urn:nhibernate-mapping-2.2">
  <class name="User">
    <id name="Id" type="Int32">
      <generator class="hilo" />
    </id>
    <property name="Name" />
    <property name="BirthDate" />
    <bag name="Blogs" inverse="true" cascade="all,delete-orphan">
      <key column="Owner" on-delete="cascade" />
      <one-to-many class="Blog" />
    </bag>
  </class>
  <class name="Blog">
    <id name="Id" type="Int32">
      <generator class="hilo" />
    </id>
    <property name="Name" />
    <many-to-one name="Owner" />
  </class>
  <class name="Comment">
    <id name="Id" type="Int32">
      <generator class="hilo" />
    </id>
    <property name="Text" />
    <many-to-one name="Author" />
    <many-to-one name="Blog" />
  </class>
</hibernate-mapping>

В общем то для существующей базы с такими маппингами уже можно работать. Как видите по умолчанию для первичных ключей используется hilo алгоритм, для коллекций используются Bag тэги, также для связи User-Blog выставляется правило каскадирования all, delete-orphan (толи это breaking change, но кажется раньше эта настройка выглядела как all-delete-orphan) и свойства называются так же как поля в базе.

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

Исходники

Apr 20, 2010

WCF NHibernate управление сессией и транзакциями

В WCF службах, как и в любых других приложениях где есть БД и ORM становится вопрос об управлении жизненным циклом объектов и транзакциями. Для собственных служб мне хотелось реализовать схему как в веб приложениях, т.е. :

  • ISession живет один запрос.
  • Транзация открывается в начале запроса, и закрывается в конце.
  • В случае наличия ошибки транзакция откатывается, в обратном подтверждается.

Реализовать это можно при помощи IDispatchMessageInspector. Данный интерфейс позволяет обработать события начала и окончания запроса. Итак реализация:

public class TransactionManager : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
    {
        SessionStorage.OpenSession();
        SessionStorage.CurrentSession.BeginTransaction(IsolationLevel.ReadCommitted);
        return null;
    }
    
    public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
    {
        var session = SessionStorage.CurrentSession;

        if (session.Transaction.IsActive)
        {
            if (reply.IsFault)
            {
                session.Transaction.Rollback();
            }
            else
            {
                session.Transaction.Commit();
            }
        }
    }
}

Чтобы зарегистрировать TransactionManager нам потребуется еще два вспомогательных класса. 1й добавит TransactionManager к службе:

public class TransactionBehaviour : IEndpointBehavior
{
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        throw new Exception("Behavior not supported on the consumer side!");
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        TransactionManager inspector = new TransactionManager();
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }
}

Второй будет использоваться для регистрации секции в конфигурационном файле:

public class TransactionBehaviorExtensionElement : BehaviorExtensionElement
{
    protected override object CreateBehavior()
    {
        return new TransactionBehaviour();
    }
    
    public override Type BehaviorType
    {
        get
        {
            return typeof(TransactionBehaviour);
        }
    }
}

И последний штрих – настройка самого сервиса где все это собирается воедино:

<system.serviceModel>
  <services>
    <service name="WcfTransactions.TestService">
      <endpoint behaviorConfiguration="transactionsEnabledBehaviour" address="" binding="basicHttpBinding" contract="WcfTransactions.ITestService"/>
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    </service>
  </services>

  <behaviors>
    <serviceBehaviors>
      <behavior>
        <serviceMetadata httpGetEnabled="true"/>
        <serviceDebug includeExceptionDetailInFaults="true"/>
      </behavior>
    </serviceBehaviors>

    <endpointBehaviors>
      <behavior name="transactionsEnabledBehaviour">
        <transactionBehaviour />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <extensions>
    <behaviorExtensions>
      <add
        name="transactionBehaviour"
        type="WcfTransactions.Transactions.TransactionBehaviorExtensionElement, WcfTransactions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </behaviorExtensions>
  </extensions>
</system.serviceModel>

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

Как по мне это слишком сложно и слишком много всего приходится делать. В Asp.net mvc есть ActionFilters которые позволяют сделать все тоже самое, но с помощью всего одного атрибута на контроллере. Подобных вещей для WCF я не нашел, буду рад если кто-то подскажет.

Разобраться в этом помогут только исходники :).

Apr 15, 2010

Сложные запросы через ICriteria

Недавно пришлось сделать довольно таки интересный запрос с использованием NHibernate Criteria API. Думаю пример будет полезен в блоге. Итак рассмотрим следующую доменную модель:

1 

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

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

  1. Имя пользователя
  2. Количество заказов
  3. Общая сумма оплат по всем заказам

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

public class SearchCriteria
{
    public string UserName { get; set; }

    public int PageSize { get; set; }

    public int PageNumber { get; set; }

    public int? OrdersNumber { get; set;}

    public double? MinPayedMoney { get; set; }

    public double? MaxPayedMoney { get; set; }
}

Он содержит текущие условия поиска. Итак начнем. Самое простое что можно реализовать это поиск по имени пользователя:

Customer customerAlias = null;  // понадобится позже
var criteria = Global.CurrentSession.CreateCriteria(typeof(Customer), () => customerAlias);

if (!string.IsNullOrEmpty(searchCriteria.UserName))
{
    criteria.Add(SqlExpression.Like<Customer>(x => x.Name, searchCriteria.UserName, MatchMode.Anywhere));
}
criteria.SetMaxResults(searchCriteria.PageSize);
criteria.SetFirstResult(searchCriteria.PageSize * (searchCriteria.PageNumber - 1));

return criteria.List<Customer>();

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

if (searchCriteria.OrdersNumber.HasValue)
{
    ICriteria ordersCriteria = criteria.CreateCriteria<Customer>(x => x.Orders);

    DetachedCriteria computersCount = DetachedCriteria.For<CustomersOrder>();
    computersCount.SetProjection(Projections.RowCount());
    computersCount.Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

    ordersCriteria.Add(Subqueries.Eq(searchCriteria.OrdersNumber.Value, computersCount));
}

Для того, чтобы Nhibernate правильно выполнил join таблиц, мы используем customerAlias. В 6й строке мы указываем на то, что нас интересуют только заказы данного пользователя, а не общее количество заказов в таблице.

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

DetachedCriteria payedAmout = DetachedCriteria.For<OrderLine>();
payedAmout.SetProjection(LambdaProjection.Sum<OrderLine>(x => x.TotalPrice));
payedAmout.CreateCriteria<OrderLine>(x => x.CustomersOrder)
    .Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

if (searchCriteria.MinPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Lt(searchCriteria.MinPayedMoney, payedAmout));
}

if (searchCriteria.MaxPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Gt(searchCriteria.MaxPayedMoney, payedAmout));
}

Собрав все воедино Nhibernate выполнит следующий SQL запрос:

SELECT top 20 this_.Id                as Id1_1_,
              this_.Name              as Name1_1_,
              customerso1_.Id         as Id3_0_,
              customerso1_.OrderDate  as OrderDate3_0_,
              customerso1_.CustomerId as CustomerId3_0_
FROM   Customers this_
       inner join CustomersOrders customerso1_
         on this_.Id = customerso1_.CustomerId
WHERE  this_.Name like '%test%' /* @p0 */
       and 2 /* @p1 */ = (SELECT count(* ) as y0_
                  FROM   CustomersOrders this_0_
                  WHERE  this_0_.CustomerId = this_.Id)
       and 100 /* @p2 */ < (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)
       and 500 /* @p3 */ > (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)

Обратите внимание на inner join в 7й строке. Если пользователь выполнил больше одного заказа, то в результате запроса SQL Server вернет столько строк, сколько заказов у пользователя. Чтобы избежать этого, необходимо добавить Distinct:

criteria.SetProjection(
                Projections.Distinct(
                    Projections.ProjectionList().Add(LambdaProjection.Property<Customer>(x => x.Id), "Id")
                                                .Add(LambdaProjection.Property<Customer>(x => x.Name), "Name")));

Теперь приведу весь код:

Customer customerAlias = null;  
var criteria = Global.CurrentSession.CreateCriteria(typeof(Customer), () => customerAlias);

if (!string.IsNullOrEmpty(searchCriteria.UserName))
{
    criteria.Add(SqlExpression.Like<Customer>(x => x.Name, searchCriteria.UserName, MatchMode.Anywhere));
}

if (searchCriteria.OrdersNumber.HasValue)
{
    ICriteria ordersCriteria = criteria.CreateCriteria<Customer>(x => x.Orders);

    DetachedCriteria computersCount = DetachedCriteria.For<CustomersOrder>();
    computersCount.SetProjection(Projections.RowCount());
    computersCount.Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

    ordersCriteria.Add(Subqueries.Eq(searchCriteria.OrdersNumber.Value, computersCount));
}

DetachedCriteria payedAmout = DetachedCriteria.For<OrderLine>();
payedAmout.SetProjection(LambdaProjection.Sum<OrderLine>(x => x.TotalPrice));
payedAmout.CreateCriteria<OrderLine>(x => x.CustomersOrder)
    .Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

if (searchCriteria.MinPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Lt(searchCriteria.MinPayedMoney, payedAmout));
}

if (searchCriteria.MaxPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Gt(searchCriteria.MaxPayedMoney, payedAmout));
}

criteria.SetProjection(
    Projections.Distinct(
        Projections.ProjectionList().Add(LambdaProjection.Property<Customer>(x => x.Id), "Id")
                                    .Add(LambdaProjection.Property<Customer>(x => x.Name), "Name")));

criteria.SetMaxResults(searchCriteria.PageSize);
criteria.SetFirstResult(searchCriteria.PageSize * (searchCriteria.PageNumber - 1));

criteria.SetResultTransformer(Transformers.AliasToBean<Customer>());

return criteria.List<Customer>();

Данный код формирует следующий SQL запрос:

SELECT distinct top 20 this_.Id   as y0_,
                       this_.Name as y1_
FROM   Customers this_
       inner join CustomersOrders customerso1_
         on this_.Id = customerso1_.CustomerId
WHERE  this_.Name like '%test%' /* @p0 */
       and 2 /* @p1 */ = (SELECT count(* ) as y0_
                  FROM   CustomersOrders this_0_
                  WHERE  this_0_.CustomerId = this_.Id)
       and 100 /* @p2 */ < (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)
       and 500 /* @p3 */ > (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)

Код выглядит как по мне страшновато. Но с другой стороны LINQ реализация врядли была бы проще. Все равно пришлось бы использовать синтаксис через extension методы и лямбда выражения.

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

Mar 30, 2010

AdoNetBatchSize и StatelessSession

Для выполнения bulk операций над базой данных в NHibernate есть специальная StatelessSession. Интерфейс данного объекта отличается от стандартного Session, у него нет методов Save, SaveOrUpdate и т.д., зато есть Insert, Update, Delete.

Особенность StatelessSession в том, что она не следит за сохранёнными объектами, значит это следующее, рассмотрим следующий пример кода:

using(var transaction = Session.BeginTransaction())
{
    var someObject = Session.Get<SomeObject>(id);
    someObject.SomeProperty = "some new value";
    transaction.Commit();
}

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

За подобное приходится платить производительностью, в частности именно это стало причиной того, что на OrmBattle Nhibernate получает далеко не самые лучшие отметки (кстати по этому поводу есть довольно хороший ответ от Ayende).

А теперь перейдем к основной теме. Показать хотелось бы следующее:

ISessionFactory sessionFactory = Fluently.Configure()
                                         .Database(MsSqlConfiguration.MsSql2008.ConnectionString("connString")
                                         .AdoNetBatchSize(40))  // important!
                                         .Mappings(x=>x.FluentMappings.AddFromAssemblyOf<TestHilo>())
                                         .BuildSessionFactory();

IStatelessSession session = sessionFactory.OpenStatelessSession();

for (int i = 0; i < 60; i++)
{
    var testHilo = new TestHilo
    {
        Field1 = "field" + i, 
        Field2 = i
    };

    session.Insert(testHilo);
}

session.Dispose();

Данный код должен вставить 60 записей в базу. На самом же деле вставится только 40. Происходит это из-за установленного AdoNetBatchSize (подробнее об этом в предыдущем посте). Когда это обнаружилось первое что захотелось сделать это вызвать session.Flush(). Но у StatelessSession нет такого метода :).

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

using (ITransaction transaction = session.BeginTransaction())
{
    for (int i = 0; i < 60; i++)
    {
        var testHilo = new TestHilo
                                {
                                    Field1 = "field" + i,
                                    Field2 = i
                                };

        session.Insert(testHilo);
    }

    transaction.Commit();
}

Такой код вставит все 60 записей.

Feb 19, 2010

Ado net batch size и идентификаторы

Для оптимизации большого количества insert-update команд NHiberante содержит специальную настройку – Ado net batch size. Рассмотрим следующий код:

for (int i = 0; i < 5000; i++)
{
    var testIncrement = new SomeEntity
                            {
                                Field1 = "field" + i, 
                                Field2 = i
                            };

    session.SaveOrUpdate(testIncrement);
} 

Какое количество запросов будет сделано к базе данных? Ответ зависит от двух условий:

  1. Установлен ли параметр ado net batch size
  2. Какой алгоритм используется для генерирования Id сущностей

Ну по поводу первого в общем то ясно – этот параметр отвечает за то, сколько запросов NHibernate объеденит в один и выполнит в один заход.

А вот о Id стоит сказать немного больше. Лично я до последнего времени во всех приложениях использовал автоинкрементные поля в Sql Server. Делалось это как наиболее простой выход для генерирования Id – в маппинге достаточно было указать GenearatedBy.Native() и все. В общем то мне никогда этот поход не мешал и вполне выполнял нужные мне задачи.

Такой же подход применялся в проекте Sharp Arhitecture… До релиза в третьем квартале 2009года :). Там алгоритмом по умолчанию установлен алгоритм hilo. Подробности этого алгоритма можно почитать по соответствующей ссылке. Основная идея заключается в следующем, когда надо получить происходит примерно следующее:

  1. В базе данных существует спец. таблица, которая хранит hi величину, это обычное число. NHiberante получает его.
  2. Nhibernate в зависимости от настроек выбирает следующую hi величину. Предположим текущее hi – 1000, а следующее 2000.
  3. Получив hi=1000 и выставив в базе данных next hi = 2000 Nhibernate в праве создавать сущности в id = 1001, 1002, 1003 и т.д.

Соответственно если каждый клиент будет обновлять hi величину, то никто из клиентов не получит одинаковые id для объектов. Для Oracle хранилищем hi значений возможно могут быть последовательности, которых нет в Sql Server.

Все это длинное описание ради того, чтобы было понятно происходящее в примере в строке 9. Как только NHibernate выполнит команду SaveOrUpdate сущность testIncrement должна получить Id. Если в качестве id используется автоинкрементное поле, то каждое такое сохранение необходимо фиксировать в базе, т.е. выполнять запрос Insert (иначе нельзя получить id). В случае же использования hilo алгоритма NHiberante может сохранить нужное количество объектов, при этом самостоятельно генерируя Id.

В общем выставив ado net batch size = 40 и выполнив код, приведенный в начале получил следующие результаты:

  • autoincrement – 5000 запросов (как и ожидалось),
  • hilo – 225.

Feb 5, 2010

NHibernate и Castle Windsor. Session per web request

Как известно основными объектами, с которыми приходится работать в NHibernate это Session и SessionFactory. При этом SessionFactory должен быть один на все приложение, поскольку его создание обходится дорого, а Session – дешевый для создания объект.

Можно рассмотреть следующий вариант, объект Session создается по необходимости – т.е. на каждый запрос к базе мы получаем новый экземпляр сессии. Но в этом сценарии мы теряем кеш первого уровня, получаем сложности с транзакциями и т.д. Наиболее распространен подход Session Per Web Request. В этом случае сессия создается в начале обработки запроса, и удаляется по завершению.

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

public class NHibernateConfiguration
{
    public ISessionFactory CreateSessionFactory()
    {
        FluentConfiguration configuration = Fluently.Configure()
                                                    .Database(MsSqlConfiguration.MsSql2008.ConnectionString(cs=>cs.FromConnectionStringWithKey("dbConnection")))
                                                    .Mappings(m=>m.FluentMappings.AddFromAssemblyOf<CategoryMap>());

        return configuration.BuildSessionFactory();
    }
}

Далее, добавив ссылки на Castle.Core, Castle.MicroKernel и Castle.Windsor создаем класс, который настроит правила работы с SessionFactory и Session:

public static class ServiceLocaterInitializer
{
    /// <summary>
    /// Initializes Castle Windsor.
    /// </summary>
    public static void Init()
    {
        IWindsorContainer container = new WindsorContainer();
        container.AddFacility<FactorySupportFacility>();
        container.Register(Component.For<ISessionFactory>()
                               .LifeStyle.Singleton
                               .UsingFactoryMethod(() => new NHibernateConfiguration().CreateSessionFactory()));

        container.Register(Component.For<ISession>()
                               .LifeStyle.PerWebRequest
                               .UsingFactoryMethod(kernel => kernel.Resolve<ISessionFactory>().OpenSession()));
    }
}

Чтобы это заработало необходимо добавить http модуль в веб конфиге:

<add name="PerRequestLifestyle" type="Castle.MicroKernel.Lifestyle.PerWebRequestLifestyleModule, Castle.MicroKernel" />

Далее, чтобы получить объект Session достаточно сделать следующий вызов:

var session = container.Resolve<ISession>();

Жизненным циклом объектов Castle будет управлять сам.

Jan 12, 2010

Маппинг деревьев в NHibernate

Доменная модель

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

public class Category
{
    private ISet<Category> childCategories;
    private Category parentCategory;

    public Category()
    {
        childCategories = new HashedSet<Category>();
    }

    public virtual int Id { get; protected set; }

    public virtual string Name { get; set; }

    public virtual ReadOnlyCollection<Category> ChildCategories
    {
        get
        {
            return new ReadOnlyCollection<Category>(new List<Category>(childCategories));
        }
    }

    public virtual Category ParentCategory
    {
        get
        {
            return parentCategory;
        }
    }

    public virtual void AddChildCategory(Category category)
    {
        if (category == null) throw new ArgumentNullException("category");
        childCategories.Add(category);
    }

    public virtual void SetParentCategory(Category category)
    {
        if (category == null) throw new ArgumentNullException("category");
        parentCategory = category;
    }
}

Каждая категория содержит ссылку на своего родителя и содержит список дочерних категорий. На данный момент модель не представляет никакой гарантии того, что дерево действительно будет деревом. Что я имею ввиду: если есть следующая вложенность: “Parent” –> “Child” -> “Child of Child”. Доменная модель никаким образом не запретит следующий код: Category(“Child Of Child”).AddChildCategory(“Parent”). Это создаст цикл и никакие рекурсивные алгоритмы без дополнительных действий работать не будут.

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

Маппинг

Теперь обратимся к Nhibernate. Маппинг для данного класса будет следующим:

public class CategoryMap : ClassMap<Category>
{
    public CategoryMap()
    {
        Id(x => x.Id).GeneratedBy.Native();

        Map(x => x.Name)
            .Length(200)
            .Not.Nullable();

        References(x => x.ParentCategory)
            .Column("ParentCategoryId")
            .Access.CamelCaseField();

        HasMany(x => x.ChildCategories)
            .Cascade.AllDeleteOrphan()
            .AsSet()
            .KeyColumn("ParentCategoryId")
            .Access.CamelCaseField();
    }
}

По маппингу можно сгенерировать следующую таблицу:

Таблица категорий

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

using (var transaction = Global.CurrentSession.BeginTransaction())
{
    Category parent = new Category();
    parent.Name = "parent";

    Category child = new Category();
    child.Name = "child";

    parent.AddChildCategory(child);

    Category childOfTheChild = new Category();
    childOfTheChild.Name = "child of the child";

    child.AddChildCategory(childOfTheChild);

    Global.CurrentSession.SaveOrUpdate(parent);
    transaction.Commit();
}

Поскольку в маппингах установлено каскадирование AllDeleteOrphan то достаточно только сохранения родительской категории. Так же это позволяет управлять поведением в случае удаления. Категория, родитель которой в данный момент удаляется будет удалена вместе с родительской. Если такое поведение вам не нужно, смените тип каскадирования на All.

Немного о производительности

Вопросы выборок иерархических структур сложны в самом SQL. Если оставить все как есть, то Nhibernate будет работать в режиме обычного lazy load и вытаскивать каждую коллекцию подкатегорий отдельным запросом. Для примера следующий код:

protected void Button1_Click(object sender, EventArgs e)
{
    ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof (Category));
    criteria.Add(Restrictions.IsNull("ParentCategory"));
    foreach (Category category in criteria.List<Category>())
    {
        EnumerateChilds(category);
    }
}

protected void EnumerateChilds(Category category)
{
    foreach (Category childCategory in category.ChildCategories)
    {
        EnumerateChilds(childCategory);
    }
}

Тут вытаскиваются все родительские категории, и мы пробегаемся по всем дочерним. Если существует следующая вложенность: “Category”->”Child Category”->”Child of child category”, то данный код выполнит запросы:

-- Выбираем родительскую категорию 
SELECT this_.Id as Id0_0_, 
       this_.Name as Name0_0_, 
       this_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] this_ 
WHERE this_.ParentCategoryId is null
-- Выборка подкатегорий каждой из категорий 
SELECT childcateg0_.ParentCategoryId as ParentCa3_1_, 
       childcateg0_.Id as Id1_, 
       childcateg0_.Id as Id0_0_, 
       childcateg0_.Name as Name0_0_, 
       childcateg0_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] childcateg0_ 
WHERE childcateg0_.ParentCategoryId=@p0   /* @p0=4 */

SELECT childcateg0_.ParentCategoryId as ParentCa3_1_, 
       childcateg0_.Id as Id1_, 
       childcateg0_.Id as Id0_0_, 
       childcateg0_.Name as Name0_0_, 
       childcateg0_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] childcateg0_ 
WHERE childcateg0_.ParentCategoryId=p0    /* @p0=5 */

SELECT childcateg0_.ParentCategoryId as ParentCa3_1_, 
       childcateg0_.Id as Id1_, 
       childcateg0_.Id as Id0_0_, 
       childcateg0_.Name as Name0_0_, 
       childcateg0_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] childcateg0_ 
WHERE childcateg0_.ParentCategoryId=@p0   /* @p0=6 */

Как правило категории нужно доставать все сразу, чтобы построить красивое дерево для навигации. На своем блоге Oren Eini показал как можно оптимизировать выборку дерева. Сделать это можно следующим запросом:

var categories = Global.CurrentSession
                       .CreateQuery("select c from Category c join fetch c.ChildCategories")
                       .SetResultTransformer(new DistinctRootEntityResultTransformer())
                       .List<Category>(); 

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

-- statement #1
select category0_.Id                 as Id0_0_,
       childcateg1_.Id               as Id0_1_,
       category0_.Name               as Name0_0_,
       category0_.ParentCategoryId   as ParentCa3_0_0_,
       childcateg1_.Name             as Name0_1_,
       childcateg1_.ParentCategoryId as ParentCa3_0_1_,
       childcateg1_.ParentCategoryId as ParentCa3_0__,
       childcateg1_.Id               as Id0__
from   [Category] category0_
       inner join [Category] childcateg1_
         on category0_.Id = childcateg1_.ParentCategoryId

-- statement #2
SELECT childcateg0_.ParentCategoryId as ParentCa3_1_,
       childcateg0_.Id               as Id1_,
       childcateg0_.Id               as Id0_0_,
       childcateg0_.Name             as Name0_0_,
       childcateg0_.ParentCategoryId as ParentCa3_0_0_
FROM   [Category] childcateg0_
WHERE  childcateg0_.ParentCategoryId = 8 /* @p0 */

-- statement #3
SELECT childcateg0_.ParentCategoryId as ParentCa3_1_,
       childcateg0_.Id               as Id1_,
       childcateg0_.Id               as Id0_0_,
       childcateg0_.Name             as Name0_0_,
       childcateg0_.ParentCategoryId as ParentCa3_0_0_
FROM   [Category] childcateg0_
WHERE  childcateg0_.ParentCategoryId = 10 /* @p0 */

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

Такое же поведение наблюдается если выключить lazy load и поставить fetch-mode=”join”.

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

Поэтому я бы предпочел использовать стандартный lazy load до тех пор, пока это не станет реальной проблемой для производительности. Как только станет заметно тормозить, то оптимизировать, либо хранимкой, либо еще как.

Пример кода.