Причем решил пойти по не самому простому пути. Вначале я решил как можно ближе ознакомится с более зрелой 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 для вызова хранимых процедур.
В начале я пробовал описать конфигурацию 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 есть две 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 IListOrders { 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 соответствует оригинальному.
На этом пожалуй хватит для одного поста. Буду очень благодарен за любой feedback, как положительный так и отрицательный.
[UPDATE]
Поменял тему в блоге на более нейтральную. Подключил google-code-prettify для подсветки синтаксиса. Надеюсь теперь будет удобне читать.
3 комментария:
Спасибо за хороший комментарий к anemic model -- помог собрать в голове картинку, так сказать.
NHibernate клевая вещь. Я очень плохознаком с entity framework, поэтому хотел спросить -- как по-вашему, насколько он догоняет по мощности NHibernate? Это приерно равные по возможностям решения?
Предлагаю на "ты", надеюсь ты не будешь против ;)
По-правде говоря я и с 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
Ну где-то вот такие у меня соображения :)
ЗЫ что-то так много написал. Может новый пост сделать :)
Отправить комментарий