воскресенье, 22 ноября 2009 г.

NHibernate. Fluent NHibernate. Первые впечатления

Не так давно я слушал доклад на тему ADO.NET Entity Framework 4.0, который читал на собрании Uneta Александр Кондуфоров. До недавнего времени мой опыт использования Entity Framework (1.0) можно было смело назвать более чем скромным. Вдохновленный услышанным докладом, я решил все таки заполнить пробелы своих знаний в области ORM.

Причем решил пойти по не самому простому пути. Вначале я решил как можно ближе ознакомится с более зрелой ORM, которая широко признана в ALT.NET сообществе - NHibernate.

В своем багаже знаний хочется иметь понимание того как строится уровень доступа к данным в наших приложениях при использовании как минимум двух ORM - NHibernate и Entity Framework. Это позволит, в случае необходимости, более взвешенно принимать решение в пользу того или иного ORM. Более того, каждый из этих ORM предполагает несколько вариантов их использования.

Например у Entity Framework 4.0 существуют как минимум три подхода: Database first, Model first, Code only. Причем можно использовать либо не использовать POCO объекты.

Довольно много информации, включая ссылки, можно почерпнуть из поста Саши Entity Framework 4.0: выходим на зрелый уровень.

В различных блогах я довольно часто встречал мнение о том, что порог вхождения в NHibernate выше чем в Entity Framework. Так же довольно часто противопоставляют Anemic Data Model у Entity Framework и Rich Data Model у NHibernate. Например в посте Ivan'а Старые песни о главном: роль ООП при работе с данными... количество комментариев перевалило за 90. :)

Что же я хотел получить от NHibernate? Вот список важных для меня моментов:
  • полная изоляция от базы данных во всех юнит-тестах. В первую очередь рассматривал mockинг DAO/Repository. Еще витали мысли об использовании предварительно подготовленной SQLite базы, но я отказался от этой идеи;

  • покрытие тестами уровня доступа к данным. Возможность написания интеграционных тестов на живую БД, которые покрывают все DAO/Repository;

  • хотел получить Persistance Ignorance как можно меньшей кровью;

  • иметь возможность использовать Linq для написания запросов, а так же какой-нибудь API для вызова хранимых процедур.
В качестве полигона для своих проб я выбрал SQLite, т.к. меня интересовал в первую очередь ORM, к тому же я не хотел устанавливать на свой домашний ноутбук никаких полновесных СУБД.

В начале я пробовал описать конфигурацию NHibernate в Application configuration file, потом описывал конфигурацию императивно в коде и в конечном счете остановился императивной конфигурации в коде с помощью библиотеки Fluent NHibernate.

Конфигурация выглядит достаточно просто и очень легко читается:
var session = Fluently.Configure()
.Database(SQLiteConfiguration.Standard
.ShowSql()
.UsingFile(@"D:\projects\DotNET\NHibernatePlayground\DB\northwindEF.db"))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf())
.BuildSessionFactory()
.OpenSession();

Так же, по мере изучения configuraion API у NHibernate, я обнаружил возможность трассировки всех SQL-запросов в лог с помощью конфигурирования трассировщика в log4net. Пока это не опробовал, но планирую это сделать. Это довольно удобно для отладки и этого очень сильно не хватало при работе с Entity Framework. Там была возможность написать свой механизм трассировки, но такого готового решения, как у NHibernate я у EF не обнаружил. Буду очень рад, если узнаю что EF это умеет и что я просто недостаточно хорошо искал.

Далее я пришел к выводу, что у NHibernate-решения существует по крайней мере три подхода для работы с данными:
  • “канонический” подход, который пришел с Hibernate. Программист описывает объектную модель в виде набора POCO-объектов. Если со стороны БД у нас есть связи, то со стороны .NET у нас будут navigation-свойства с типизированными коллециями. Меппинг между .NET и БД описывается в специальных *.hbm.xml-файлах. Вполне возможно, этот подход можно было бы назвать удобным, если бы у него была мощная поддержка в Visual Studio в виде визуального дизайнера;

  • проект Castle ActiveRecord. Однако я эти проекты не рассматривал, т.к. ActiveRecord влияет на мою доменную модель. При его использовании мы должны помечать свои классы и свойства атрибутами, которые описывают меппинг к БД, а это нарушает Persistence Ignorance. В любом случае, этот подход вполне имеет право на жизнь в небольших проектах;

  • описание меппинга с помощью Fluent NHibernate. Мне этот вариант понравился больше всего.
В Fluent NHibernate меппинг описывается на C# языке, что упрощает рефакторинг, предполагает наличие IntelliSense, а так же минимальную проверку грубых оплошностей в виде compile-time errors.

Кроме того, у Fluent NHibernate есть две killer-фичи - это Auto Mapping + Conventions и Persistence specification testing.

Auto Mapping я пока не использовал и пошел по пути “медленно но верно”. В качестве исходной БД я взял базу Northwind. По мере описания меппинга, я удивился насколько мощными возможностями обладает NHibernate. Например, NHibernate умеет круто меппить иерархии классов - table per class hierarchy, table per subclass, table per concrete class. Не знаю, насколько Fluent NHibernate покрывает возможности, заложенные в hbm.xml-меппинге, но он помог мне описать все нужные мне правила, не смотря на то, что я ни в коем случае не пытался прогнуть доменную модель под схему базы. Весь API по меппингу у Fluent NHibernate можно посмотреть по этой ссылке.

Вот например как у меня выглядит класс Customer:
public class Customer
{
public virtual string ID { get; set; }
public virtual string CompanyName { get; set; }
public virtual string ContactName { get; set; }
public virtual string ContactTitle { get; set; }
public virtual string Address { get; set; }
public virtual string City { get; set; }
public virtual string Region { get; set; }
public virtual string PostalCode { get; set; }
public virtual string Country { get; set; }
public virtual string Phone { get; set; }
public virtual string Fax { get; set; }
public virtual IList Orders { get; private set; }
}

Соответственно, меппинг этого класса выглядит вот так:
public class CustomerMap : ClassMap
{
public CustomerMap()
{
Table("Customers");
Id(x => x.ID).Column("CustomerID").Length(5);
Map(x => x.CompanyName).Length(40).Not.Nullable();
Map(x => x.ContactName).Length(30);
Map(x => x.ContactTitle).Length(30);
Map(x => x.Address).Length(60);
Map(x => x.City).Length(15);
Map(x => x.Region).Length(15);
Map(x => x.PostalCode).Length(10);
Map(x => x.Country).Length(15);
Map(x => x.Phone).Length(24);
Map(x => x.Fax).Length(24);
HasMany(x => x.Orders).KeyColumn("CustomerID");
}
}

Учитывая, что меппинг описывается вручную, то для того чтобы быть 100% уверенным в его корректности, необходимы тесты. Команда Fluent NHibernate подумала и над этой проблемой и предложила решение в виде Persistence specification testing, который моем случае выглядит следующим образом:
[TestMethod]
public void Save_Customer_in_database()
{
RemoveCustomerIfExists("A");
new PersistenceSpecification(TestHelper.GetSession())
.CheckProperty(c => c.ID, "A")
.CheckProperty(c => c.CompanyName, "TestCompanyName")
.CheckProperty(c => c.ContactName, "TestContactName")
.CheckProperty(c => c.ContactTitle, "TestContactTitle")
.CheckProperty(c => c.Address, "TestAddress")
.CheckProperty(c => c.City, "TestCity")
.CheckProperty(c => c.Region, "TestRegion")
.CheckProperty(c => c.PostalCode, "TestPostalCode")
.CheckProperty(c => c.Country, "TestCountry")
.CheckProperty(c => c.Phone, "TestPhone")
.CheckProperty(c => c.Fax, "TestFax")
.VerifyTheMappings();
}

Этот тест делает следующее:
  • приводит БД в предсостояние для теста (удаляет кастомера, если он уже существует);

  • создает экземпляр Customer с заданными параметрами;

  • вставляет данные по этому кастомеру в базу данных;

  • извлекает из базы данных запись в другой экземпляр класса Customer;

  • проверяет что полученный Customer соответствует оригинальному.
Еще стоило бы немного рассказать о HQL, Criteria API, NHibernate.Linq, Query Batcher и о случае “вау! какой же умный этот NHibernate”, но это не сейчас.

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

[UPDATE]
Поменял тему в блоге на более нейтральную. Подключил google-code-prettify для подсветки синтаксиса. Надеюсь теперь будет удобне читать.

3 комментария:

Meowth комментирует...

Спасибо за хороший комментарий к anemic model -- помог собрать в голове картинку, так сказать.

NHibernate клевая вещь. Я очень плохознаком с entity framework, поэтому хотел спросить -- как по-вашему, насколько он догоняет по мощности NHibernate? Это приерно равные по возможностям решения?

Alexey Diyan комментирует...

Предлагаю на "ты", надеюсь ты не будешь против ;)

По-правде говоря я и с NHibernate и c Entity Framework знаком одинаково плохо. NHibernate пока только изучаю, на боевых проектах не применял. Entity Framework 1.0 у нас на работе на одном из проектов, с которым я пересекаюсь, но плотно с ним тоже не работал. Поэтому к моему мнению стоит отнестись с осторожностью ;)

Из того что мне довелось увидеть-услышать-пощупать могу сказать что
- Entity Framework 1.0 (.NET 3.5) очень далек по мощи от NHibernate, хотя 4.0 (.NET 4.0) версия очень существенно доработана в плане тестируемости кода
- У EF есть проблемы с edmx-файлами. После обновления схемы БД его можно либо перегенерировать, либо править вручную edmx. Т.е. если проект большой то могут возникнуть конфликты разделения ресурса. В NHibernate меппинг описывается отдельно для каждой сущности.
- У EF 1.0 есть метод Include для явной загрузки, но он не типизированный.В принципе есть способы это обойти, хотя они довольно нетривиальные.
- Не могу сказать с уверенностью, но по-моему возможности мэппинга у EF 4.0 не дотягивают до NHibernate
- У EF 4.0 появился fluent syntax для описания мэппинга. Говорят что он удобный.
- Из отчета ORM Battle создается впечатление что EF 1.0 быстрее чем NH.
- У NH плохо с визуальными дизайнерами, хотя если честно то мне кажется что продвинутый мэппинг в сочетании automapping несколько скрашивают это упущение
- NH выглядит как сборная солянка - NHibernate / ActiveRecord / Fluent NHibernate, NHibernate.Linq / HQL / CriteriaAPI. EF выглядит как монолитное решение.
- EF 1.0 не умеет POCO, но к счатью EF 4.0 уже умеет
- NHibernate.Linq все еще несколько сыроват. Как известно Linq-провайдеры в Microsoft обкатывали на Lin2Sql, а это значит что у EF есть уже некий опыт в этом. Даже не смотря на то что L2S и EF писали разные команды
- NH довольно тяжело изучать. Мне он показался сложнее EF, хотя и мощнее
- EF это Microsoft, а значит это mainstream и больше востребован. NH это ALT.NET

Ну где-то вот такие у меня соображения :)

ЗЫ что-то так много написал. Может новый пост сделать :)

Анонимный комментирует...
Этот комментарий был удален администратором блога.