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

Пример кода.

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. если кто-то найдет решение вопроса со сменой типа – огромная просьба поделиться :)