Страницы

суббота, 19 ноября 2011 г.

NHibernate. Вникаем в hilo генератор.

  Для большинства пользователей NHibernate hilo генератор остаётся туманным, и они предпочитают старый недобрый identity. Давайте разберём их его по косточкам.
Сама аббревиатура hilo обозначает High Low, которые обозначают два числа. На примере его можно объяснить так: программа запрашивает в некоем хранилище уникальное High число. Допустим, это будет 5. При этом Low число равно 9. Тогда мы можем безопасно присваивать в качестве идентификатора числа 50,51,52,...59. После исчерпания чисел приложение запрашивает новое число High. Такой механизм позволяет безопасно присваивать идентификаторы на стороне клиента, даже для множества клиентов. Таков сам принцип генератора hilo, теперь посмотрим, как он реализован в nhibernate.
  Для начала образец нашего наш "домена":
public class Cat
{
  public virtual int Id { get; set; }

  public virtual string Name { get; set; }
}
и его маппинг:
<class name="Cat">
  <id name="Id">
    <generator class="hilo">
    <param name="table">HighNumbers</param>
    <param name="column">NextHigh</param>
    <param name="max_lo">4</param>
    </generator>
  </id>
  <property name="Name"/>
</class>
  • table- название таблице, содержащей информацию о текущем High числе.
  • column - название колонки, содержащей информацию о текущем High числе.
  • max_lo - максимально low число, которое захватывает nhibernate.
  Рассмотрим на живых примерах. Пустим в дело такой код:
[Test]
public void SaveManyCats()
{
  using(var session = _sessionFactory.OpenSession())
  using (var tx = session.BeginTransaction())
  {
    for (int i = 0; i < 15; i++)
    {
      session.Persist(new Cat{Name = "Cat number " + i});
    }
    tx.Commit();
  }
}
Для начала выполним его с identity-генератором. И вот что будет в логах:

2011-11-25 23:06:19,288 DEBUG [7] - (SQL) :
    INSERT 
    INTO 
        dbo.Cat 
        (Name) 
    VALUES 
        (@p0);     select SCOPE_IDENTITY();     @p0 = 'Cat number 0' [Type: String (4000)]

2011-11-25 23:06:19,384 DEBUG [7] - (SQL) :
    INSERT 
    INTO 
        dbo.Cat 
        (Name) 
    VALUES 
        (@p0);     select SCOPE_IDENTITY();     @p0 = 'Cat number 1' [Type: String (4000)]

... и еще шесть таких записей. 

 
  Nhibernate после сохранения объекта в бд, присваивает объекту в памяти идентификатор. И чтобы знать значение этого идентификатора, дописывает к запросу select SCOPE_IDENTITY(); Таким образом Nhibernate выполняет сохранение одного объекта за одно обращение к БД, расходуя машинные ресурсы, как олигархи деньги в Куршавеле! А чем же лучше hilo? Снова взглянем на логи. Во-первых, видны обращения для получения High числа, которые регистрируются в таблице HighNumbers .


2011-11-25 23:20:17,734 DEBUG [7] - (SQL) : Reading high value:
    select 
        NextHigh 
    from 
        [dbo].HighNumbers 
    with (updlock,rowlock)

2011-11-25 23:20:17,799 DEBUG [7] - (SQL) : Updating high value:
    update 
        [dbo].HighNumbers 
    set 
        NextHigh = @p0 
    where 
        NextHigh = @p1;     @p0 = 2 [Type: Int32 (0)], @p1 = 1 [Type: Int32 (0)]

2011-11-25 23:20:17,871 DEBUG [7] - (SQL) : Reading high value:
    select 
        NextHigh 
    from 
        [dbo].HighNumbers 
    with (updlock, rowlock)
 

2011-11-25 23:20:17,874 DEBUG [7] - (SQL) : Updating high value:     update 
        [dbo].HighNumbers 
    set 
        NextHigh = @p0 
    where 
        NextHigh = @p1;     @p0 = 3 [Type: Int32 (0)], @p1 = 2 [Type: Int32 (0)]

Обращений могло быть и меньше, но я специально выбрал max_lo = 5, чтобы была видна работа hilo генератора. Так происходит вставка:

2011-11-25 23:20:17,929 DEBUG [7] - (SQL) : Batch commands:
command 0:
    INSERT
    INTO
        dbo.Cat
        (Name, Id)
    VALUES
        (@p0, @p1);
    @p0 = 'Cat number 0' [Type: String (4000)], @p1 = 5 [Type: Int32 (0)]
command 1:
    INSERT
    INTO
        dbo.Cat
        (Name, Id)
    VALUES
        (@p0, @p1);
    @p0 = 'Cat number 1' [Type: String (4000)], @p1 = 6 [Type: Int32 (0)]
... и еще шесть команд.


  Всё внимание  выделенным словах: NHibernate вставил все 8 записей в одном пакете! Это удалось сделать, потому что hilo генератор присваивает значения на клиенте.
  Присвоенные идентификаторы - 5,6.. и т.д. Как они получились? Дело в том, что nhibernate  высчитывает диапазон по формуле high * (max_lo + 1)..high * (max_lo + 1) + max_lo. В нашем случае, для первого зарезервированного числа high = 1, диапазон был 5..9, и для втого high = 2  10..14.
  И еще несколько слов о hilo. Зарезервированный диапазон чисел хранится на уровне ISessionFactory,  и доступ к нему потокобезопасен. Если приложение закрылось, не выбрав весь диапазон чисел - он пропадает. Но не стоит об этом беспокоится (если конечно вы не установили слишком большое max_lo). А если всё-таки предел близок - берите Int64. Их хватит на все разумные размеры приложений. Еще можно использовать фильтр where. Только помните, что на одну иерархию классов должен быть один фильтр (об этом ниже).
  Identity генератор не позволяет замапить иерархию классов через union-subclass, т.к. в этом случае нет никакой ясности, как искать базовый класс по идентификатору, если он есть в нескольких таблицах наследников. В случае hilo такой проблемы нет - идентификаторы уникальны.

  Надеюсь, мне удалось приоткрыть завесу тайны над hilo. Если у вас есть nhibernate - забудьте identity как страшный сон! Hilo намного эффективней, и даёт больше возможностей.

четверг, 17 ноября 2011 г.

Конфликт имён. И как с ним бороться.

Проблема - конфликт имён. 
 
  Однажды, в обычный рабочий день писал unit-тест для проверки работы Windsor-interceptor'ов с помощью Moq. Код выглядел примерно так:
var system = new Mock<ISystem>();
var windsor = new WindsorContainer();

windsor.Register(
  Component.For<ISystem>()
    .Instance(system.Object)
    .Interceptors(typeof (SomeInterceptor)));

var resolvedSystem = windsor.Resolve<ISystem>();

var proxy = (IProxyTargetAccessor) resolvedSystem;
  В ответ компилятор вполне определённо намекает:

 Error    1    The type 'Castle.DynamicProxy.IProxyTargetAccessor' exists in both 'd:\Projects\Test\packages\Castle.Core.3.0.0.2001\lib\net40-client\Castle.Core.dll' and 'd:\Projects\Test\packages\Moq.4.0.10827\lib\NET40\Moq.dll' 
  
  Дело в том, что в Moq интегрирована сборка Castle.DynamicProxy. Дописать namespace не поможет - имена совпадают полностью. Аналогичная проблема возникает и в случае Rhino.Mocks. Огорчительно, но к счастью, решение есть.


Решение - Alias. 

  Если посмотреть свойства любого reference в проекте, можно заметить одно волшебное свойство:

  По-умолчанию значение этого свойства global. Что оно означает? В некотором смысле, это самый глобальный namespace, с которого компилятор начинает поиск зависимостей. Т.е. такой код вполне компилируется и рабоает:
var windsor = new global::Castle.Windsor.WindsorContainer();
  Так вот, в свойствах reference можно присвоить зависимости другой alias.  Поменяем для Moq название на Moq.
  Теперь в коде мы можем ссылаться на этот псевдоним. Для этого служит ключевое слово extern alias. Для этого служит ключевое слово extern alias. Добавить его следует в самое начало файла:
extern alias Moq;
using Moq::Moq;
  Вторая строка выглядит несколько странно, но я объясню: это обычный using, но с указанием alias'a библиотеки. (другие using мы можем писать global::System.Text и т.д., но компилятор разрешает нам опустить global).
  Таким же образом можно использовать несколько версий одной библиотеки. Хотя и не рекомендовал бы этого делать. 

  Пожалуй, стоит упомянуть о самом главном - недостатках. Если для reference указан alias - то в каждом файле, где он используется придётся писать extern alias. Поэтому подумайте хорошенько, что какую именно библиотеку надо переименовать.