Страницы

понедельник, 30 января 2012 г.

NHibernate. Mapping By Code. Вся модель в одну строчку.

  С появлением в Nhibernate 3.2 Mapping By Code маппить стало гораздо легче, приятней и быстрее (MappingByCode не требует десериализации xml). Но хотелось большего, что всё как-нибудь само замапилось. Это возможно без лишних усилий (спасибо Фабио за ConventionMapper), но соглашения по-умолчанию отличаются от моих собственных. Вот этот недостаток я решил исправить в проекте Enhima.
Использовать ее очень легко:
var config = new Configuration(); // Это обычный класс конфигурации NHibernate
config.MapEntities(From.ThisApplication()); // А это весь маппинг
  Enhima будет искать все сущности, находящиеся в текущем домене приложений, и автоматически маппить их по соглашениям.
 Я не буду рассказывать про все соглашения, их можно найти в коде. Но пара слов о главном:
Enhima ищет все объекты, наследующие класс Enhima.Entity. Это упрощённая версия AbstractEntity из проекта uNhAddIns. Но есть еще одна - сущность AggregateRoot. Enhima использует их для управления каскадным сохранением удалением, согласно идеям Эрика Эванса из книги DDD.
  Соглашения - это хорошо и быстро. Но что если соглашений недостаточно? Это не проблема, Enhima переопределит значения из маппинга по соглашениям значениями из ClassByClass маппингов (ClassMapping<T>, SubclassMapping<T>, UnionSubclassMapping<T>, JoinSubclassMapping<T>)  без дополнительных усилий.

 Полученные маппинги неплохо было бы протестировать. Для этого есть еще один метод:
var config = new Configuration(); // Это обычный класс конфигурации NHibernate
config.СonfigureSqlite(); // Для тестирования с файлом БД.
config.СonfigureSqliteInmemory(); // Для тестирования в памяти.
 Такая конфигурация вряд ли подойдёт для развёртывания, но для быстрых интеграционных тестов вполне.  В последнее время любой приличный проект под .net обязан иметь свой nuget. Enhima не исключение :) Правда, пока она находиться в стадии beta-тестирования. Поэтому для установки в проект стоит воспользоваться ключом -pre:
install-package enhima.entities -pre
install-package enhima.mapping -pre
Удачных маппингов!

среда, 25 января 2012 г.

MSBuild. TransformXml. Маленькая, но весьма досадная проблема.

  Недавно пришлось занимать простой вещью: есть проект, есть шаблон конфигурации. И для каждого развертывания системы надо заполнить свой шаблон. Ок, у нас есть решение. Берём TransformXml-Task для msbuild, и трансформируем конфиг:
<TransformXml 
  Source="someapp.config" 
  Transform="transform.xml" 
  Destination="someapp.config" />
  Выглядит просто, не так ли? Увы только выглядит. Сама по себе задача преобразования работает замечательно, только вот  TransformXml блокирует... Source! Чтобы понять, как это приятно попробуйте наложить несколько трансформаций на один файл. В итоге у меня вышло нечто подобное:
<Copy 
  SourceFiles="someapp.config" 
  DestinationFiles="someapp.config.1"/>
<TransformXml 
  Source="someapp.config.1" 
  Transform="transform1.xml" 
  Destination="someapp.config" />
<Copy   SourceFiles="someapp.config"   DestinationFiles="someapp.config.2"/> <TransformXml   Source="someapp.config.2"   Transform="transform2.xml"   Destination="someapp.config" />
Не очень элегантно. И временные файлы нельзя удалить в процессе работы msbuild - они заблокированы. Весьма, весьма досадный баг, и молниеносная реакция на него. Может кто-нибудь знает способ лучше?

суббота, 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. Поэтому подумайте хорошенько, что какую именно библиотеку надо переименовать.

вторник, 25 октября 2011 г.

NHibernate vs Entity Framework 4.

  Недавно пришлось делать сравнение NHiberante и EF4. Стоит отметить, что мой личный опыт включает в себя только использование NHibernate, и остальные изыскание проводились исключительно с позиции стоит ли EF4 усилий по своему изучению. Вкратце преимущество каждого перечислены ниже.


Преимущества NHibernate
  1. Пакетное чтение (MulityQuery, Future)
  2. Пакетная запись, настраиваемая в конфиге. 
  3. Пакетная ленивая загрузка коллекций, сокращающая проблему N +1 
  4. Сверхленивая загрузка коллекций (Order.OrderLines.Count приводит к select count(*) )
  5. Возможность постраничной выборки и фильтрации коллекций. 
  6. Разнообразные способы выборки: HQL, ICriteria, QueryOver, LINQ, SQL(включая хранимые процедуры) и трансформации результатов с использованием IResultTransformer.
  7. Кеш второго уровня. 
  8. Большое количество точек расширения, открывающих широкие возможности по модификации поведения 
  9. Как следствие из предыдущего пункта, существует большое большое количество расширяющих фреймворков. Примеры:
    • NHibernate.Envers - аудит 
    • NHibernate.Validator - думаю понятно :)
    • NHiberante.Search - Полнотекстовый поиск с использованием Lucene.Net
    • NHibernate.Shards - горизонтальное масштабирование приложений. 
    • Rhino.Security - библиотека для разграничения прав доступа. 
  10. Разнообразные способы маппинга объектов на бд:
    • xml
    • аттрибуты
    • маппинг кодом (2 отдельных фреймворка + встроенная поддержка)
      • по классам
      • по соглашениям.
  11. Более мощный язык объектных запросов (HQL vs Entity SQL), позволяющий выполнять объектные DML-команды без загрузки объектов в память. 
  12. Встроенная поддержка для логгирования генерируемых sql-команд. 
  13. Более 10 разнообразных id-генераторов, включая наиболее эффективные для ORM HiLo и guid.comb.
  14. Возможность маппинга разнообразных пользовательских типов (шифрованные строки, локализованные свойства, запись Enum как строки, и многое другое)
  15. Поддержка readonly свойств. 
  16. Поддеркжа коллекций элементов (например IList<int>)
  17. Поддержка маппинга словарей (Dictionary).
  18. Сильное сообщество программистов. 



Преимущества EF
  1. Лучшая поддержка LINQ.
  2. Свойства объектов не обязательно должны быть виртуальными.  
  3. Microsoft.
  4. Наличие визуального редактора от Microsoft.
Мой выбор очевиден, NHibernate. Остальное решать вам.